i18n - docs translations (#21789)

Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
github-actions[bot]
2026-06-18 15:21:04 +02:00
committed by GitHub
parent 22baf2c6c5
commit 2b3b2362db
704 changed files with 38268 additions and 9002 deletions
@@ -11,7 +11,7 @@ title: 사용자 지정 개체
## 고급 스키마
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="고급 스키마" />
<img src="/images/docs/server/custom-object-schema.png" alt="고급 스키마" />
</div>
<br />
@@ -27,7 +27,7 @@ title: 사용자 지정 개체
사용자 지정 개체를 추가하려면 /metadata API를 쿼리하십시오. 이는 메타데이터를 해당 API에 따라 업데이트하고, 메타데이터를 기반으로 GraphQL 스키마를 계산하여 나중에 사용할 수 있도록 GQL 캐시에 저장합니다.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/add-custom-objects.jpeg" alt="/metadata API를 쿼리하여 사용자 지정 개체를 추가하십시오." />
<img src="/images/docs/server/add-custom-objects.jpeg" alt="/metadata API를 쿼리하여 사용자 지정 개체를 추가하십시오." />
</div>
<br />
@@ -35,5 +35,5 @@ title: 사용자 지정 개체
데이터를 가져오려면 /graphql 엔드포인트를 통해 쿼리를 수행하고 Query Resolver를 통해 전달하는 과정이 포함됩니다.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="/graphql 엔드포인트를 쿼리하여 데이터를 가져오십시오." />
<img src="/images/docs/server/custom-object-schema.png" alt="/graphql 엔드포인트를 쿼리하여 데이터를 가져오십시오." />
</div>
@@ -1,5 +1,6 @@
---
title: 백엔드 명령어
icon: terminal
---
## 유용한 명령어
@@ -43,6 +44,13 @@ npx nx run twenty-server:database:reset
```
### 마이그레이션
#### Core/Metadata 스키마의 객체용 (TypeORM)
```bash
npx nx run twenty-server:database:migrate:generate
```
## 기술 스택
Twenty는 주로 NestJS를 백엔드에 사용합니다.
@@ -43,7 +43,9 @@ cp .env.example .env
## 개발
<Warning>
`zapier` 명령어를 실행하기 전에 반드시 `yarn build`를 실행하세요.
`zapier` 명령어를 실행하기 전에 반드시 `yarn build`를 실행하세요.
</Warning>
### 테스트
@@ -1,5 +1,6 @@
---
title: 버그, 요청 및 Pull Request
icon: 버그
info: 이슈를 보고하고, 기능을 요청하고, 코드에 기여하세요
---
@@ -1,5 +1,6 @@
---
title: 모범 사례
icon: 별
---
이 문서는 프론트엔드 작업 시 따라야 할 모범 사례를 설명합니다.
@@ -8,12 +9,14 @@ title: 모범 사례
React와 Jotai는 코드베이스에서 상태 관리를 처리합니다.
### `useAtomState`로 상태 저장하기
### Jotai 아톰으로 상태 저장하기
상태를 저장하는 데 필요한 만큼의 atom을 만드는 것이 좋은 습관입니다.
<Warning>
프롭스 드릴링에 너무 많은 노력을 기울이기보다는 추가 아톰을 사용하는 것이 좋습니다.
프롭스 드릴링에 너무 많은 노력을 기울이기보다는 추가 아톰을 사용하는 것이 좋습니다.
</Warning>
```tsx
@@ -43,7 +46,7 @@ export const MyComponent = () => {
상태 저장에 `useRef`를 사용하지 않도록 주의하십시오.
If you want to store state, you should use `useState` or `useAtomState`.
상태를 저장하려면 `useState`를 사용하거나 `useAtomState`와 함께 Jotai 아톰을 사용해야 합니다.
일부 리렌더링을 방지하기 위해 `useRef`가 필요하다고 느낄 경우 [리렌더링 관리 방법](#managing-re-renders)을 참조하십시오.
@@ -130,9 +133,9 @@ export const App = () => (
);
```
### Jotai 가족 상태 및 Jotai 가족 선택자 사용하기
### 아톰 가족 상태 및 선택자 사용하기
Jotai 가족 상태와 선택자는 리렌더링을 피하기 위한 훌륭한 방법입니다.
아톰 가족 상태와 선택자는 리렌더링을 피하기 위한 훌륭한 방법입니다.
항목 목록을 저장해야 할 때 유용합니다.
@@ -1,5 +1,6 @@
---
title: 폴더 아키텍처
icon: folder-tree
info: 우리의 폴더 아키텍처를 자세히 살펴보기
---
@@ -84,7 +85,7 @@ module1
상태 관리 로직이 포함되어 있습니다. [Jotai](https://jotai.org)가 이것을 처리합니다.
* 셀렉터: 파생 아톰(`createAtomSelector` 사용)은 다른 아톰에서 값을 계산하며 자동으로 메모이제이션됩니다.
* 셀렉터: 파생 atom(`createAtomSelector` 사용)은 다른 atom으로부터 값을 계산하며 자동으로 메모이제이션됩니다.
React의 내장 상태 관리는 구성 요소 내에서의 상태를 여전히 처리합니다.
@@ -1,5 +1,6 @@
---
title: 프론트엔드 명령어
icon: 터미널
---
## 유용한 명령어
@@ -77,7 +78,7 @@ To avoid unnecessary [re-renders](/l/ko/developers/contribute/capabilities/front
### 상태 관리
[Jotai](https://jotai.org/) 상태 관리를 처리합니다.
[Jotai](https://jotai.org/) 상태 관리를 처리합니다.
상태 관리에 대한 자세한 정보는 [최고의 관례](/l/ko/developers/contribute/capabilities/frontend-development/best-practices-front#state-management)를 참조하십시오.
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
}
```
내부적으로, 현재 선택한 스코프는 애플리케이션 전반에 걸쳐 공유되는 Jotai 상태에 저장됩니다:
내부적으로, 현재 선택한 스코프는 애플리케이션 전반에 걸쳐 공유되는 Jotai atom에 저장됩니다:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
});
```
하지만 이 Jotai 상태는 수동으로 처리해서는 안 됩니다! 다음 섹션에서 사용하는 방법을 배웁니다.
하지만 이 atom은 수동으로 처리해서는 안 됩니다! 다음 섹션에서 사용하는 방법을 배웁니다.
## 내부적으로 어떻게 작동합니까?
[react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) 위에 얇은 래퍼를 만들어 성능을 높이고 불필요한 재랜더링을 방지합니다.
또한 핫키 스코프 상태를 처리하고 애플리케이션 전반에서 사용할 수 있도록 Jotai 상태를 만듭니다.
또한 핫키 스코프 상태를 처리하고 애플리케이션 전반에서 사용할 수 있도록 Jotai atom을 만듭니다.
@@ -1,5 +1,6 @@
---
title: 스타일 가이드
icon: 페인트 브러시
---
이 문서에는 코드 작성 시 따라야 할 규칙이 포함되어 있습니다.
@@ -8,7 +9,7 @@ title: 스타일 가이드
이를 위해서는 너무 간결하기보다는 약간 더 장황한 것이 낫습니다.
항상 사람들이 코드를 작성하는 것보다 더 자주 읽는다는 점을 염두에 두십시오. 특히 누구나 기여할 수 있는 오픈 소스 프로젝트에서 그렇습니다.
항상 사람들이 코드를 작성하는 것보다 더 자주 읽는다는 점을 염두에 두십시오. 특히 누구나 기여할 수 있는 오픈 소스 프로젝트에서는 더욱 그렇습니다.
여기에 정의되지 않은 많은 규칙이 있지만 린터에 의해 자동으로 확인됩니다.
@@ -150,7 +151,7 @@ type MyType = {
### 열거형 대신 문자열 리터럴 사용
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types)는 TypeScript에서 열거형과 같은 값을 처리하는 주요 방법입니다. 들은 Pick Omit으로 확장하기 쉬우며, 특히 코드 완성 기능을 사용할 때 더 나은 사용자 경험을 제공합니다.
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types)는 TypeScript에서 열거형과 같은 값을 처리하는 주요 방법입니다. 들은 Pick Omit으로 확장하기 더 쉽고, 특히 코드 자동 완성 기능을 사용할 때 더 나은 개발자 경험을 제공합니다.
타입스크립트가 열거형 사용을 피하도록 권장하는 이유는 [여기](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums)에서 확인할 수 있습니다.
@@ -260,7 +261,7 @@ const StyledButton = styled.button`
## No-Type Import 강제 실행
타입 import를 피하십시오. 이 표준을 강하기 위해 Oxlint 규칙이 모든 타입 import를 확인하고 보고합니다. 이는 TypeScript 코드의 일관성과 가독성을 유지하는 데 도움이 됩니다.
타입 import를 피하십시오. 이 표준을 강하기 위해 Oxlint 규칙이 모든 타입 import를 검사하고 보고합니다. 이는 TypeScript 코드의 일관성과 가독성을 유지하는 데 도움이 됩니다.
```tsx
// ❌ Bad
@@ -283,8 +284,8 @@ import { Meta, StoryObj } from '@storybook/react';
### Oxlint 규칙
Oxlint 규칙인 `typescript/consistent-type-imports`는 타입 없는 import 표준을 강제합니다. 이 규칙은 모든 타입 import 위반에 대해 오류나 경고를 생성합니다.
Oxlint 규칙인 `typescript/consistent-type-imports`는 타입 import 금지 표준을 강제합니다. 이 규칙은 모든 타입 import 위반에 대해 오류나 경고를 생성합니다.
이 규칙은 의도치 않은 타입 import가 발생하는 드문 경계 사례를 구체적으로 다룹니다. 타입스크립트 자체는 [TypeScript 3.8 릴리스 노트](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html)에서 이 관행을 권장하지 않습니다. 대부분의 상황에서 타입 전용 import를 사용할 필요가 없습니다.
이 규칙을 준수하는 코드를 보장하기 위해 Oxlint를 개발 워크플로의 일부로 실행하십시오.
코드가 이 규칙을 준수하도록 개발 워크플로의 일부로 Oxlint를 실행하십시오.
@@ -13,7 +13,9 @@ Figma는 디자이너와 개발자 간의 소통 장벽을 허무는 협업 인
전문 모드 및 전용 프레임 선택 기능과 같이 로그인한 사용자에게만 제공되는 주요 기능이 있습니다.
<Warning>
계정 없이는 효과적으로 협업할 수 없습니다.
계정 없이는 효과적으로 협업할 수 없습니다.
</Warning>
## Figma 구조
@@ -1,71 +1,72 @@
---
title: 로컬 설정
icon: laptop-code
description: 콘트리뷰터(기여자)나 호기심 많은 개발자를 위한 가이드로서, Twenty를 로컬에서 실행하고자 하는 분들에게 드리는 안내입니다.
---
## 사전 준비
<Tabs>
<Tab title="리눅스 및 맥OS">
Twenty를 설치하고 사용하기 전에, 먼저 컴퓨터에 다음을 설치하세요:
<Tab title="Linux 및 macOS">
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node v24.5.0](https://nodejs.org/en/download)
* [yarn v4](https://yarnpkg.com/getting-started/install)
* [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
Twenty를 설치하고 사용하기 전에, 먼저 컴퓨터에 다음을 설치하세요:
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node v24.5.0](https://nodejs.org/en/download)
* [yarn v4](https://yarnpkg.com/getting-started/install)
* [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
<Warning>
`npm`은 사용할 수 없으며, 대신 `yarn`을 사용해야 합니다. Yarn은 이제 Node.js와 함께 제공되기 때문에 별도로 설치할 필요가 없습니다.
아직 하지 않았다면 `corepack enable`을 실행하여 Yarn을 사용할 수 있도록 설정하세요.
</Warning>
</Tab>
<Warning>
`npm`은 사용할 수 없으며, 대신 `yarn`을 사용해야 합니다. Yarn은 이제 Node.js와 함께 제공되기 때문에 별도로 설치할 필요가 없습니다.
아직 하지 않았다면 `corepack enable`을 실행하여 Yarn을 사용할 수 있도록 설정하세요.
</Warning>
<Tab title="윈도우 (WSL)">
1. WSL 설치
PowerShell을 관리자 권한으로 열고 다음을 실행하세요:
</Tab>
```powershell
wsl --install
```
<Tab title="윈도우 (WSL)">
이제 컴퓨터를 재시작하라는 프롬프트가 나타날 것입니다. 그렇지 않으면 수동으로 재시작하세요.
1. WSL 설치
PowerShell을 관리자 권한으로 열고 다음을 실행하세요:
```powershell
wsl --install
```
이제 컴퓨터를 재시작하라는 프롬프트가 나타날 것입니다. 그렇지 않으면 수동으로 재시작하세요.
시작 후, powershell 창이 열리고 Ubuntu가 설치됩니다. 이 작업은 다소 시간이 걸릴 수 있습니다.
Ubuntu 설치 시 사용자 이름과 암호를 만드는 프롬프트가 나타납니다.
다시 시작 후 PowerShell 창이 열리고 Ubuntu가 설치됩니다. 이 작업은 다소 시간이 걸릴 수 있습니다.
Ubuntu 설치 시 사용자 이름과 암호를 만드는 프롬프트가 나타납니다.
2. Git 설치 및 구성
2. Git 설치 및 구성
```bash
sudo apt-get install git
```bash
sudo apt-get install git
git config --global user.name "Your Name"
git config --global user.name "Your Name"
git config --global user.email "youremail@domain.com"
```
git config --global user.email "youremail@domain.com"
```
3. nvm, node.js 및 yarn 설치
3. nvm, node.js 및 yarn 설치
<Warning>
적절한 `node` 버전을 설치하기 위해 `nvm`을 사용하세요. `.nvmrc`는 모든 기여자가 동일한 버전을 사용하도록 보장합니다.
</Warning>
<Warning>
적절한 `node` 버전을 설치하기 위해 `nvm`을 사용하세요. `.nvmrc`는 모든 기여자가 동일한 버전을 사용하도록 보장합니다.
</Warning>
```bash
sudo apt-get install curl
```bash
sudo apt-get install curl
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
```
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
```
nvm을 사용하려면 터미널을 닫았다가 다시 여세요. 그런 다음 다음 명령어를 실행하세요.
nvm을 사용하려면 터미널을 닫았다가 다시 여세요. 그런 다음 다음 명령어를 실행하세요.
```bash
```bash
nvm install # installs recommended node version
nvm install # installs recommended node version
nvm use # use recommended node version
nvm use # use recommended node version
corepack enable
```
corepack enable
```
</Tab>
</Tab>
</Tabs>
---
@@ -75,19 +76,19 @@ description: 콘트리뷰터(기여자)나 호기심 많은 개발자를 위한
터미널에서 다음 명령어를 실행하세요.
<Tabs>
<Tab title="SSH (권장)">
아직 SSH 키를 설정하지 않은 경우, [여기](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh)를 참조하여 설정하는 방법을 배우세요.
<Tab title="SSH (권장)">
아직 SSH 키를 설정하지 않은 경우, [여기](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh)를 참조하여 설정하는 방법을 배우세요.
```bash
git clone git@github.com:twentyhq/twenty.git
```
</Tab>
<Tab title="HTTPS">
```bash
git clone git@github.com:twentyhq/twenty.git
```
</Tab>
```bash
git clone https://github.com/twentyhq/twenty.git
```
<Tab title="HTTPS">
```bash
git clone https://github.com/twentyhq/twenty.git
```
</Tab>
</Tab>
</Tabs>
## 2단계: 루트 위치 설정
@@ -103,21 +104,17 @@ cd twenty
<Tabs>
<Tab title="리눅스">
**옵션 1 (권장):** 데이터베이스를 로컬에서 프로비저닝하려면:
Linux 기기에 Postgresql을 설치하려면 다음 링크를 사용하십시오: [Postgresql 설치](https://www.postgresql.org/download/linux/)
Linux 머신에 PostgreSQL을 설치하려면 다음 링크를 사용하세요: [PostgreSQL 설치](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
참고: 권한 오류를 피하기 위해 `psql` 명령어 앞에 `sudo -u postgres`를 추가해야 할 수 있습니다.
**옵션 2:** 도커가 설치된 경우:
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="맥 OS">
**옵션 1 (권장):** `brew`로 로컬에서 데이터베이스를 프로비저닝하려면:
@@ -129,17 +126,15 @@ cd twenty
```
PostgreSQL 서버가 실행 중인지 확인하려면 다음을 실행하세요:
```bash
brew services list
```
설치 시 기본적으로 `postgres` 사용자가 생성되지 않을 수 있습니다.
macOS에서 Homebrew로 설치할 경우, 설치 시스템 계정과 일치하는 PostgreSQL 역할이 생성됩니다(예: "john"). 대신 사용자의 macOS
macOS에서 Homebrew로 설치할 때
설치 프로그램이 기본적으로 `postgres` 사용자를 생성하지 않을 수 있습니다. 대신 사용자의 macOS
사용자 이름(예: "john")과 일치하는 PostgreSQL 역할을 생성합니다.# PostgreSQL 접속psql postgres
또는
psql -U $(whoami) -d postgres
```bash
# Connect to PostgreSQL
psql postgres
@@ -148,70 +143,61 @@ cd twenty
```
psql 프롬프트에서(postgres=#), 다음을 실행하세요:
```bash
# List existing PostgreSQL roles
\du
```
```bash
# List existing PostgreSQL roles
\du
```
출력은 다음과 비슷해야 합니다:
```bash
Role name | Attributes | Member of
-----------+-------------+-----------
john | Superuser | {}
```
```bash
Role name | Attributes | Member of
-----------+-------------+-----------
john | Superuser | {}
```
`postgres` 역할이 나열되지 않으면 다음 단계를 계속하십시오.
`postgres` 역할을 수동으로 만드세요:
```bash
CREATE ROLE postgres WITH SUPERUSER LOGIN;
```
```bash
CREATE ROLE postgres WITH SUPERUSER LOGIN;
```
이는 로그인 액세스가 있는 슈퍼유저 역할 `postgres`를 생성합니다.
```bash
역할 이름 | 속성 | 소속
-----------+-------------+-----------
postgres | 슈퍼유저 | {}
john | 슈퍼유저 | {}
```
```bash
역할 이름 | 속성 | 소속
-----------+-------------+-----------
postgres | 슈퍼유저 | {}
john | 슈퍼유저 | {}
```
**옵션 2:** 도커가 설치된 경우:
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="윈도우 (WSL)">
다음 단계는 모두 WSL 터미널(가상 머신 내)에서 실행됩니다.
**옵션 1:** Postgresql을 로컬에서 프로비저닝하려면:
Linux 가상 머신에 Postgresql을 설치하려면 다음 링크를 사용하세요: [Postgresql 설치](https://www.postgresql.org/download/linux/)
**옵션 1:** PostgreSQL을 로컬에서 프로비저닝하려면:
Linux 가상 머신에 PostgreSQL을 설치하려면 다음 링크를 사용하세요: [PostgreSQL 설치](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
참고: 권한 오류를 피하기 위해 `psql` 명령어 앞에 `sudo -u postgres`를 추가해야 할 수 있습니다.
**옵션 2:** 도커가 설치된 경우:
WSL에서 Docker를 실행하면 추가적인 복잡성이 발생합니다.
이 옵션은 [Docker Desktop WSL2](https://docs.docker.com/desktop/wsl)를 켠 상태에서의 추가 설정 단계를 수반할 수 있다는 점에서 편안할 때만 사용하십시오.
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
</Tabs>
이제 [localhost:5432](localhost:5432)에서 데이터베이스에 액세스할 수 있으며, 사용자 `postgres`와 비밀번호 `postgres` 를 사용합니다.
이제 `localhost:5432`에서 데이터베이스에 접속할 수 있니다.
위의 Docker 옵션을 사용했다면, 기본 자격 증명은 사용자 `postgres`와 비밀번호 `postgres`입니다. 네이티브 PostgreSQL 설치의 경우, 로컬 머신에 구성된 자격 증명과 역할을 사용하세요.
## 4단계: Redis 데이터베이스 (캐시) 설정
Twenty는 최상의 성능을 제공하기 위해 redis 캐시가 필요합니다.
Twenty는 최상의 성능을 제공하기 위해 Redis 캐시가 필요합니다.
<Tabs>
<Tab title="리눅스">
@@ -219,42 +205,37 @@ Twenty는 최상의 성능을 제공하기 위해 redis 캐시가 필요합니
리눅스 기기에 Redis를 설치하려면 다음 링크를 사용하십시오: [Redis 설치](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
**옵션 2:** 도커가 설치된 경우:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="맥 OS">
**옵션 1 (권장):** `brew`로 Redis를 로컬에서 프로비저닝하려면:
```bash
brew install redis
```
redis 서버를 시작하세요:
`brew services start redis`
Redis 서버를 시작하세요:
```bash
brew services start redis
```
**옵션 2:** 도커가 설치된 경우:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="윈도우 (WSL)">
**옵션 1:** Redis를 로컬에서 프로비저닝하려면:
Linux 가상 머신에 Redis를 설치하려면 다음 링크를 사용하세요: [Redis 설치](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
**옵션 2:** 도커가 설치된 경우:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
</Tabs>
클라이언트 GUI가 필요한 경우에는 [redis insight](https://redis.io/insight/) (무료 버전 제공)을 권장합니다.
클라이언트 GUI가 필요하다면 [Redis Insight](https://redis.io/insight/)를 권장합니다(무료 버전 제공).
## 5단계: 환경 변수 설정
@@ -268,7 +249,7 @@ cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env
```
<Info>
**멀티 워크스페이스 모드:** 기본적으로 Twenty는 하나의 워크스페이스만 생성할 수 있는 단일 워크스페이스 모드로 실행됩니다. 멀티 워크스페이스 지원을 활성화하려면(서브도메인 기반 기능을 테스트할 때 유용합니다), 서버의 `.env` 파일에서 `IS_MULTIWORKSPACE_ENABLED=true`로 설정하세요. 자세한 내용은 [멀티 워크스페이스 모드](/l/ko/developers/self-host/capabilities/setup#multi-workspace-mode)를 참조하세요.
**멀티 워크스페이스 모드:** 기본적으로 Twenty는 하나의 워크스페이스만 생성할 수 있는 단일 워크스페이스 모드로 실행됩니다. 멀티 워크스페이스 지원을 활성화하려면(서브도메인 기반 기능을 테스트할 때 유용합니다), 서버의 `.env` 파일에서 `IS_MULTIWORKSPACE_ENABLED=true`로 설정하세요. 자세한 내용은 [멀티 워크스페이스 모드](/l/ko/developers/self-host/capabilities/setup#multi-workspace-mode)를 참조하세요.
</Info>
## 6단계: 의존성 설치
@@ -288,15 +269,12 @@ yarn
리눅스 배포판에 따라 Redis 서버가 자동으로 시작될 수 있습니다.
시작되지 않는다면, 각 배포판에 맞는 [Redis 설치 가이드](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/)를 확인하세요.
</Tab>
<Tab title="맥 OS">
Redis가 이미 실행 중이어야 합니다. 그렇지 않으면 다음을 실행하세요:
Redis가 이미 실행 중이어야 합니다. 그렇지 않으면 다음을 실행하세요:
```bash
brew services start redis
```
</Tab>
<Tab title="윈도우 (WSL)">
리눅스 배포판에 따라 Redis 서버가 자동으로 시작될 수 있습니다.
시작되지 않는다면, 각 배포판에 맞는 [Redis 설치 가이드](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/)를 확인하세요.
@@ -0,0 +1,77 @@
---
title: 명령
icon: terminal
description: Twenty 개발에 유용한 명령어.
---
`npx nx`를 사용해 리포지토리 루트에서 명령을 실행할 수 있습니다. 명시적으로 대상 지정하려면 `npx nx run {project}:{command}`를 사용하세요.
## 앱 시작하기
```bash
npx nx start twenty-front # Frontend dev server (http://localhost:3001)
npx nx start twenty-server # Backend server (http://localhost:3000)
npx nx run twenty-server:worker # Background worker
```
## 데이터베이스
```bash
npx nx database:reset twenty-server # Reset and seed database
npx nx run twenty-server:database:migrate:prod # Run migrations
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow> # Generate a migration
```
## 린팅
```bash
npx nx lint:diff-with-main twenty-front # Lint changed files (fastest)
npx nx lint:diff-with-main twenty-server
npx nx lint twenty-front --configuration=fix # Auto-fix
```
## 타입 검사
```bash
npx nx typecheck twenty-front
npx nx typecheck twenty-server
```
## 테스트
```bash
# Frontend
npx nx test twenty-front # Jest unit tests
npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:test twenty-front # Storybook tests
# Backend
npx nx run twenty-server:test:unit # Unit tests
npx nx run twenty-server:test:integration # Integration tests
npx nx run twenty-server:test:integration:with-db-reset # Integration with DB reset
# Single file (fastest)
npx jest path/to/test.test.ts --config=packages/{project}/jest.config.mjs
```
## GraphQL
```bash
npx nx run twenty-front:graphql:generate # Regenerate types
npx nx run twenty-front:graphql:generate --configuration=metadata # Metadata schema
```
## 번역
```bash
npx nx run twenty-front:lingui:extract # Extract strings
npx nx run twenty-front:lingui:compile # Compile translations
```
## 빌드
```bash
npx nx build twenty-shared # Must be built first
npx nx build twenty-front
npx nx build twenty-server
```
@@ -25,7 +25,6 @@ Twenty는 오픈 소스로, 커뮤니티의 기여를 환영합니다. 버그를
<Card title="버그 보고 및 요청" icon="bug" href="/l/ko/developers/contribute/capabilities/bug-and-requests">
문제를 보고하거나 기능을 요청하세요
</Card>
<Card title="프론트엔드 개발" icon="browser" href="/l/ko/developers/contribute/capabilities/frontend-development">
UI에 기여하세요
</Card>
@@ -0,0 +1,176 @@
---
title: 스타일 가이드
icon: 페인트 브러시
description: Twenty에 기여하기 위한 코드 컨벤션과 모범 사례.
---
## 리액트
### 함수형 컴포넌트만 사용
항상 네임드 export가 있는 TSX 함수형 컴포넌트를 사용하십시오.
```tsx
// ❌ Bad
const MyComponent = () => {
return <div>Hello World</div>;
};
export default MyComponent;
// ✅ Good
export function MyComponent() {
return <div>Hello World</div>;
};
```
### 프로퍼티
이름이 `{ComponentName}Props`인 타입을 생성하십시오. 구조 분해 할당을 사용하십시오. `React.FC`를 사용하지 마십시오.
```tsx
type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
### 단일 변수 prop 스프레딩 금지
```tsx
// ❌ Bad
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
// ✅ Good
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
```
## 상태 관리
### 전역 상태를 위한 Jotai 아톰
```tsx
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
defaultValue: 'default value',
});
```
* prop 드릴링보다 아톰을 선호하십시오.
* 상태에는 `useRef`를 사용하지 말고 — `useState` 또는 아톰을 사용하십시오
* 목록에는 아톰 패밀리와 셀렉터를 사용하십시오
### 불필요한 리렌더링을 피하십시오
* `useEffect`와 데이터 패칭을 형제 사이드카 컴포넌트로 분리하십시오
* `useEffect`보다 이벤트 핸들러(`handleClick`, `handleChange`)를 선호하십시오
* `React.memo()`를 사용하지 말고 — 근본 원인을 수정하십시오
* `useCallback` / `useMemo` 사용을 제한하십시오
```tsx
// ❌ Bad — useEffect in the same component causes re-renders
export const Page = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => { setData(dep); }, [dep]);
return <div>{data}</div>;
};
// ✅ Good — extract into sibling
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => { setData(dep); }, [dep]);
return <></>;
};
export const Page = () => {
const [data] = useAtomState(dataState);
return <div>{data}</div>;
};
```
## 타입스크립트
* **`interface`보다 `type`** — 더 유연하고 조합하기 쉽습니다
* **열거형보다 문자열 리터럴** — GraphQL codegen enum 및 내부 라이브러리 API는 예외
* **`any` 금지** — 엄격한 TypeScript를 강제합니다
* **타입 import 금지** — 일반 import를 사용하십시오 (Oxlint `typescript/consistent-type-imports`로 강제)
* **[Zod](https://github.com/colinhacks/zod)을 사용**하여 타입이 없는 객체를 런타임에 검증하십시오
## 자바스크립트
```tsx
// Use nullish-coalescing (??) instead of ||
const value = process.env.MY_VALUE ?? 'default';
// Use optional chaining
onClick?.();
```
## 이름 지정
* **변수**: camelCase, 의미를 잘 드러내도록 작성 (`value`가 아닌 `email`, `fm`이 아닌 `fieldMetadata`)
* **상수**: SCREAMING_SNAKE_CASE
* **타입/클래스**: PascalCase
* **파일/디렉터리**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
* **이벤트 핸들러**: `handleClick` (핸들러 함수 이름은 `onClick`이 아님)
* **컴포넌트 props**: 컴포넌트 이름을 접두사로 붙이십시오 (`ButtonProps`)
* **스타일드 컴포넌트**: `Styled` 접두사를 붙이십시오 (`StyledTitle`)
## 스타일링
[Linaria](https://github.com/callstack/linaria) 스타일드 컴포넌트를 사용하십시오. 테마 값을 사용하십시오 — 하드코딩된 `px`, `rem` 또는 색상은 피하십시오.
```tsx
// ❌ Bad
const StyledButton = styled.button`
color: #333333;
font-size: 1rem;
margin-left: 4px;
`;
// ✅ Good
const StyledButton = styled.button`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
```
## 가져오기
상대 경로 대신 별칭을 사용하십시오:
```tsx
// ❌ Bad
import { Foo } from '../../../../../testing/decorators/Foo';
// ✅ Good
import { Foo } from '~/testing/decorators/Foo';
import { Bar } from '@/modules/bar/components/Bar';
```
## 폴더 구조
```
front
└── modules/ # Feature modules
│ └── module1/
│ ├── components/
│ ├── constants/
│ ├── contexts/
│ ├── graphql/ (fragments, queries, mutations)
│ ├── hooks/
│ ├── states/ (atoms, selectors)
│ ├── types/
│ └── utils/
└── pages/ # Route-level components
└── ui/ # Reusable UI components (display, input, feedback, ...)
```
* 모듈은 다른 모듈에서 import할 수 있지만, `ui/`는 의존성이 없어야 합니다
* 모듈 전용 코드는 `internal/` 하위 폴더를 사용하십시오
* 컴포넌트는 300줄 이하, 서비스는 500줄 이하
@@ -0,0 +1,55 @@
---
title: API
icon: plug
description: 워크스페이스 스키마에서 생성된 REST 및 GraphQL API.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
## 테넌트별 스키마 API
Twenty에는 정적 API 참조 문서가 없습니다. 각 워크스페이스는 고유한 스키마를 가집니다 — 사용자 지정 객체(예: `Invoice`)를 추가하면, `Company` 또는 `Person`과 같은 기본 제공 객체와 동일한 REST 및 GraphQL 엔드포인트가 즉시 생성됩니다. API는 스키마에서 생성되므로 엔드포인트에서 객체와 필드 이름을 직접 사용합니다 — 의미를 알 수 없는 ID는 없습니다.
워크스페이스별 API 문서는 API 키 생성 후 **설정 → API 및 웹훅**에서 확인할 수 있습니다. 데이터에 대해 실제 호출을 실행할 수 있는 대화형 플레이그라운드가 포함되어 있습니다.
## 두 가지 API
**핵심 API** — `/rest/` 및 `/graphql/`
레코드에 대한 CRUD: People, Companies, Opportunities, 사용자 지정 객체. 쿼리, 필터링, 관계 탐색.
**메타데이터 API** — `/rest/metadata/` 및 `/metadata/`
스키마 관리: 객체, 필드, 관계 생성/수정/삭제. 이것이 데이터 모델을 프로그래밍 방식으로 변경하는 방법입니다.
둘 다 REST와 GraphQL로 사용할 수 있습니다. GraphQL은 배치 업서트와 단일 쿼리에서 관계를 탐색하는 기능을 제공합니다. 어떤 방식을 사용하든 동일한 기본 데이터입니다.
## 기본 URL
| 환경 | 기본 URL |
| ------ | ------------------------- |
| 클라우드 | `https://api.twenty.com/` |
| 셀프 호스팅 | `https://{your-domain}/` |
## 인증
```
Authorization: Bearer YOUR_API_KEY
```
**Settings → API & Webhooks → + Create key**에서 API 키를 생성하세요. 즉시 복사하세요 — 한 번만 표시됩니다. 키는 **Settings → Members → Roles → Assignment tab**에서 특정 역할에 범위를 지정하여 액세스 가능한 범위를 제한할 수 있습니다.
<VimeoEmbed videoId="928786722" title="API 키 생성" />
사용자 대신 동작하는 외부 앱을 위한 OAuth 기반 액세스에 대해서는 [OAuth](/l/ko/developers/extend/oauth)를 참고하세요.
## 배치 작업
REST와 GraphQL 모두 요청당 최대 60개의 레코드에 대한 배치 처리를 지원합니다 — 생성, 업데이트 또는 삭제. GraphQL은 `CreateCompanies`와 같은 복수형 이름을 사용하여 배치 업서트(한 번의 호출로 생성 또는 업데이트)도 지원합니다.
## 속도 제한
| 제한 | 값 |
| ----- | ------------ |
| 요청 | 분당 100회 호출 |
| 배치 크기 | 호출당 60개의 레코드 |
@@ -0,0 +1,64 @@
---
title: 애플리케이션 구성
description: "`defineApplication`을 사용하여 앱의 식별 정보, 기본 역할, 변수 및 마켓플레이스 메타데이터를 선언합니다."
icon: rocket
---
모든 앱에는 `defineApplication` 호출이 정확히 하나 있어야 합니다. 다음 항목을 선언합니다:
* **식별 정보** — 범용 식별자, 표시 이름, 설명.
* **권한** — 로직 함수와 프런트 컴포넌트가 어떤 역할로 실행되는지.
* **변수** *(선택 사항)* — 코드에 환경 변수로 노출되는 키–값 쌍.
* **설치 전/후 훅** *(선택 사항)* — [Logic Functions](/l/ko/developers/extend/apps/logic/logic-functions)를 참조하세요.
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
});
```
노트:
* `universalIdentifier` 필드는 여러분이 소유하는 변하지 않는 고유 ID입니다. 한 번만 생성하고 이후 동기화 동안에도 변하지 않도록 유지하세요.
* `applicationVariables`는 함수와 프런트 컴포넌트의 환경 변수가 됩니다. 로직 함수(서버 사이드)에서는 `process.env.VARIABLE_NAME`으로 사용할 수 있습니다. 프런트 컴포넌트에서는 `twenty-sdk/front-component`의 `getApplicationVariable('VARIABLE_NAME')`을 사용하세요. `isSecret: true`로 표시된 변수는 로직 함수에만 주입됩니다. 프런트 컴포넌트는 비밀이 아닌 변수만 받습니다.
* 기본 역할은 [`defineApplicationRole()`](/l/ko/developers/extend/apps/config/roles)로 표시된 역할 파일에서 자동으로 감지되므로, `defineApplication()`에서 이를 참조할 필요가 없습니다.
* 설치 전/후 함수는 매니페스트 빌드 중 자동으로 감지됩니다 — `defineApplication()`에서 별도로 참조할 필요가 없습니다.
* 하위 호환성을 위해 `defaultRoleUniversalIdentifier`를 명시적으로 전달하는 방식도 계속 지원되지만, 이제는 `defineApplicationRole()` 사용을 권장하며 이전 방식은 더 이상 권장되지 않습니다.
## 기본 함수 역할
[`defineApplicationRole()`](/l/ko/developers/extend/apps/config/roles)로 선언된 역할은 앱의 로직 함수와 프런트엔드 컴포넌트가 무엇에 접근할 수 있는지를 제어합니다:
* `TWENTY_APP_ACCESS_TOKEN`로 주입되는 런타임 토큰은 이 역할에서 파생됩니다.
* 타입드 API 클라이언트는 해당 역할에 부여된 권한으로 제한됩니다.
* 최소 권한 원칙을 따르세요: 함수에 필요한 권한만 선언하세요.
새 앱을 스캐폴딩하면 CLI가 `src/roles/default-role.ts`에 시작용 역할 파일을 생성합니다. 전체 내용은 [Roles & Permissions](/l/ko/developers/extend/apps/config/roles)를 참조하세요.
## 마켓플레이스 메타데이터
앱을 [게시](/l/ko/developers/extend/apps/operations/publishing)할 계획이라면, 다음 선택적 필드로 마켓플레이스에서 표시되는 방식을 제어합니다:
| 필드 | 설명 |
| ------------------ | ------------------------------------------------------------------- |
| `author` | 작성자 또는 회사 이름 |
| `category` | 마켓플레이스 필터링을 위한 앱 카테고리 |
| `logoUrl` | 앱 로고의 경로(예: `public/logo.png`) |
| `screenshots` | 스크린샷 경로의 배열(예: `public/screenshot-1.png`) |
| `aboutDescription` | "About" 탭에 대한 더 긴 마크다운 설명. 생략하면 마켓플레이스는 npm의 패키지 `README.md`를 사용합니다 |
| `websiteUrl` | 웹사이트 링크 |
| `termsUrl` | 서비스 약관 링크 |
| `emailSupport` | 지원 이메일 주소 |
| `issueReportUrl` | 이슈 트래커 링크 |
@@ -0,0 +1,206 @@
---
title: 설치 훅
description: 설치 전에나 후에 로직을 실행하여 시드 데이터를 추가하고, 레코드를 백업하고, 업그레이드를 검증하세요.
icon: wrench
---
설치 훅은 설치 또는 업그레이드 라이프사이클 동안 실행되는 특수한 로직 함수입니다. 이들은 일반 [로직 함수](/l/ko/developers/extend/apps/logic/logic-functions)와 동일한 핸들러 런타임을 공유하고 `InstallPayload`를 받지만, 자체 정의 함수인 `definePostInstallLogicFunction()` 및 `definePreInstallLogicFunction()`으로 선언되며, 일반 트리거 모델(HTTP, cron, 데이터베이스 이벤트) 외부에서 동작합니다.
각 앱은 **최대 하나의 pre-install** 함수와 **최대 하나의 post-install** 함수만 정의할 수 있습니다. 둘 이상이 감지되면 매니페스트 빌드에서 오류가 발생합니다.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="워크스페이스 메타데이터 마이그레이션이 적용된 후에 실행됩니다.">
설치 후 함수는 워크스페이스에 앱 설치가 완료된 뒤 자동으로 실행되는 로직 함수입니다. 서버는 앱의 메타데이터가 동기화되고 SDK 클라이언트가 생성된 **이후** 이를 실행하므로, 워크스페이스는 완전히 사용할 준비가 되었고 새 스키마가 적용된 상태입니다. 일반적인 사용 사례로는 기본 데이터를 시드하는 것, 초기 레코드를 생성하는 것, 워크스페이스 설정을 구성하는 것, 서드파티 서비스에서 리소스를 프로비저닝하는 것 등이 있습니다.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
CLI를 사용하여 언제든지 설치 후 함수를 수동으로 실행할 수도 있습니다:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
```
핵심 요점:
* 설치 후 함수는 `definePostInstallLogicFunction()`을 사용합니다 — 트리거 설정(`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`)을 생략한 특수 변형입니다.
* 핸들러는 `{ previousVersion?: string; newVersion: string }` 형태의 `InstallPayload`를 받습니다 — `newVersion`은 현재 설치 중인 버전이고, `previousVersion`은 이전에 설치되었던 버전입니다(처음 설치인 경우에는 `undefined`). 이 값들을 사용하여 신규 설치와 업그레이드를 구분하고, 버전별 마이그레이션 로직을 실행하세요.
* **훅이 실행되는 시점**: 기본적으로 신규 설치에서만 실행됩니다. 이전 버전에서 앱이 업그레이드될 때도 실행되게 하려면 `shouldRunOnVersionUpgrade: true`를 전달하세요. 생략하면 플래그는 기본값 `false`가 되며, 업그레이드 시 훅을 건너뜁니다.
* **실행 모델 — 기본은 비동기, 동기는 선택적**: `shouldRunSynchronously` 플래그는 설치 후 작업이 *어떤 방식으로* 실행되는지 제어합니다.
* `shouldRunSynchronously: false` *(기본값)* — 훅은 `retryLimit: 3`와 함께 **메시지 큐에 등록**되며 워커에서 비동기적으로 실행됩니다. 작업이 큐에 등록되는 즉시 설치 응답이 반환되므로, 처리 속도가 느리거나 실패하는 핸들러가 호출자를 차단하지 않습니다. 워커는 최대 세 번까지 재시도합니다. **장시간 실행되는 작업에 사용하세요** — 대규모 데이터셋 시딩, 느린 서드파티 API 호출, 외부 리소스 프로비저닝 등 합리적인 HTTP 응답 시간 창을 초과할 수 있는 모든 작업.
* `shouldRunSynchronously: true` — 훅이 **설치 플로우 중에 인라인으로** 실행됩니다(설치 전과 동일한 실행기). 핸들러가 완료될 때까지 설치 요청이 블록되고, 예외가 발생하면 설치 호출자는 `POST_INSTALL_ERROR`를 받습니다. 자동 재시도 없음. **응답 전에 반드시 완료되어야 하는 빠른 작업에 사용하세요** — 예: 사용자에게 검증 오류를 표시하거나, 설치 호출이 반환된 직후 클라이언트가 즉시 의존하는 빠른 설정. post-install이 실행될 시점에는 메타데이터 마이그레이션이 이미 적용되었음을 유의하세요. 따라서 동기 모드에서 실패하더라도 스키마 변경이 **롤백되지 않으며**, 오류만 노출됩니다.
* 핸들러가 멱등적임을 보장하세요. 비동기 모드에서는 큐가 최대 세 번까지 재시도할 수 있습니다. 어떤 모드이든 `shouldRunOnVersionUpgrade: true`인 경우 업그레이드 시 훅이 다시 실행될 수 있습니다.
* 환경 변수 `APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`은 핸들러 내부에서 사용할 수 있습니다(다른 로직 함수와 동일). 따라서 앱에 범위가 지정된 애플리케이션 액세스 토큰으로 Twenty API를 호출할 수 있습니다.
* 애플리케이션당 설치 후 함수는 하나만 허용됩니다. 둘 이상이 감지되면 매니페스트 빌드에서 오류가 발생합니다.
* 함수의 `universalIdentifier`, `shouldRunOnVersionUpgrade`, `shouldRunSynchronously`는 빌드 중에 애플리케이션 매니페스트의 `postInstallLogicFunction` 필드에 자동으로 첨부됩니다 — 따라서 [`defineApplication()`](/l/ko/developers/extend/apps/config/application)에서 이들을 참조할 필요가 없습니다.
* 기본 시간 제한은 데이터 시딩과 같은 더 긴 설정 작업을 허용하기 위해 300초(5분)로 설정되어 있습니다.
* **개발 모드에서 실행되지 않음**: 앱이 로컬로 등록된 경우(`yarn twenty dev`), 서버는 설치 플로우를 완전히 건너뛰고 CLI 워처를 통해 파일을 직접 동기화합니다 — 따라서 `shouldRunSynchronously` 여부와 관계없이 개발 모드에서는 post-install이 절대 실행되지 않습니다. 실행 중인 워크스페이스에 대해 수동으로 트리거하려면 `yarn twenty dev:function:exec --postInstall`을 사용하세요.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="워크스페이스 메타데이터 마이그레이션이 적용되기 전에 실행됩니다.">
pre-install 함수는 설치 중에 자동으로 실행되는 로직 함수로, **워크스페이스 메타데이터 마이그레이션이 적용되기 전에** 실행됩니다. post-install과 동일한 페이로드 형태(`InstallPayload`)를 사용하지만, 설치 플로우에서 더 이른 단계에 위치하여 곧 진행될 마이그레이션이 의존하는 상태를 준비할 수 있습니다 — 일반적인 사용 사례로는 데이터 백업, 새 스키마와의 호환성 검증, 재구조화되거나 삭제될 레코드의 보관 등이 있습니다.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
CLI를 사용하여 언제든지 설치 전 함수를 수동으로 실행할 수도 있습니다:
```bash filename="Terminal"
yarn twenty dev:function:exec --preInstall
```
핵심 요점:
* pre-install 함수는 `definePreInstallLogicFunction()`을 사용합니다 — post-install과 동일한 특수화된 구성을 사용하되, 서로 다른 라이프사이클 슬롯에 연결됩니다.
* pre-install과 post-install 핸들러는 동일한 `InstallPayload` 타입을 받습니다: `{ previousVersion?: string; newVersion: string }`. 한 번만 임포트하여 두 훅에서 재사용하세요.
* **훅이 실행되는 시점**: 워크스페이스 메타데이터 마이그레이션(`synchronizeFromManifest`) 직전. 실행에 앞서, 서버는 워크스페이스 메타데이터에 **새로운** 버전의 pre-install 함수를 등록하는 순수 추가식의 "간소화된 동기화"를 수행합니다 — 그 외에는 아무것도 변경하지 않습니다 — 그리고 나서 이를 실행합니다. 이 동기화는 추가 전용이므로, 핸들러가 실행될 때 이전 버전의 객체, 필드, 데이터는 그대로 유지됩니다. 따라서 마이그레이션 이전 상태를 안전하게 읽고 백업할 수 있습니다.
* **실행 모델**: pre-install은 **동기적으로** 실행되며 **설치를 차단**합니다. 핸들러에서 예외를 던지면, 어떤 스키마 변경도 적용되기 전에 설치가 중단되며 — 워크스페이스는 일관된 상태로 이전 버전에 머무릅니다. 이는 의도된 동작입니다: pre-install은 위험한 업그레이드를 거부할 수 있는 마지막 기회입니다.
* post-install과 마찬가지로, 애플리케이션당 pre-install 함수는 하나만 허용됩니다. 빌드 중에 애플리케이션 매니페스트의 `preInstallLogicFunction` 아래에 자동으로 연결됩니다.
* **개발 모드에서 실행되지 않음**: post-install과 동일하게 — 로컬로 등록된 앱은 설치 플로우가 완전히 건너뛰어지므로 `yarn twenty dev` 환경에서 pre-install은 실행되지 않습니다. `yarn twenty dev:function:exec --preInstall`를 사용하여 수동으로 트리거하세요.
</Accordion>
<Accordion title="pre-install vs post-install: 언제 어떤 것을 사용할지" description="올바른 설치 훅 선택하기">
두 훅 모두 동일한 설치 플로우의 일부이며 같은 `InstallPayload`를 받습니다. 차이점은 워크스페이스 메타데이터 마이그레이션과의 상대적인 실행 **시점**이며, 이에 따라 안전하게 다룰 수 있는 데이터가 달라집니다.
pre-install은 항상 **동기식**입니다(설치를 차단하고 중단할 수 있음). post-install은 **기본적으로 비동기식**입니다 — 워커에 큐잉되고 자동 재시도가 수행됩니다 — 하지만 `shouldRunSynchronously: true`로 동기 실행을 선택할 수 있습니다. 각 모드를 언제 사용할지에 대해서는 위의 `definePostInstallLogicFunction` 아코디언을 참고하세요.
**새로운 스키마의 존재가 필요한 작업에는 `post-install`을 사용하세요.** 일반적인 경우입니다:
* 새로 추가된 객체와 필드를 대상으로 기본 데이터를 시딩(초기 레코드, 기본 보기, 데모 콘텐츠 생성)하는 작업.
* 앱에 자격 증명이 생겼으므로 서드파티 서비스에 웹훅을 등록하는 작업.
* 동기화된 메타데이터에 의존하는 설정을 완료하기 위해 자체 API를 호출하는 작업.
* 모든 업그레이드마다 상태를 조정해야 하는 멱등적인 "존재함을 보장(ensure this exists)" 로직 — `shouldRunOnVersionUpgrade: true`와 함께 사용하세요.
예시 — 설치 후 기본 `PostCard` 레코드를 시딩하기:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**마이그레이션으로 인해 기존 데이터가 손실되거나 손상될 우려가 있을 때는 `pre-install`을 사용하세요.** pre-install은 *이전* 스키마에 대해 실행되고 실패 시 업그레이드를 롤백하므로, 위험한 작업에 적합합니다:
* **곧 삭제되거나 재구조화될 데이터를 백업** — 예: v2에서 필드를 제거하므로, 마이그레이션이 실행되기 전에 해당 값을 다른 필드로 복사하거나 스토리지로 내보내야 하는 경우.
* **새로운 제약으로 인해 무효화될 레코드를 보관** — 예: 어떤 필드가 `NOT NULL`로 바뀌어 null 값을 가진 행을 먼저 삭제하거나 수정해야 하는 경우.
* **호환성을 검증하고, 현재 데이터를 깔끔하게 마이그레이션할 수 없는 경우 업그레이드를 거부** — 핸들러에서 예외를 던지면 아무 변경도 적용되지 않은 채 설치가 중단됩니다. 이는 마이그레이션 도중에 비호환성을 발견하는 것보다 더 안전합니다.
* 연관이 끊어질 수 있는 스키마 변경에 앞서 **데이터 이름 변경 또는 키 재지정**.
예시 — 파괴적인 마이그레이션 전에 레코드 보관하기:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**경험칙:**
| 원하는 작업... | 사용 |
| -------------------------------------- | ---------------------------------------------------------------- |
| 기본 데이터 시딩, 워크스페이스 구성, 외부 리소스 등록 | `post-install` |
| 설치 응답을 차단해서는 안 되는 장시간 시딩 또는 서드파티 호출 실행 | `post-install` (기본 — `shouldRunSynchronously: false`, 워커 재시도 포함) |
| 설치 호출이 반환된 직후 호출자가 즉시 의존하는 빠른 설정 실행 | `shouldRunSynchronously: true`를 사용하는 `post-install` |
| 곧 진행될 마이그레이션으로 손실될 데이터를 읽거나 백업 | `pre-install` |
| 기존 데이터를 손상시킬 업그레이드를 거부 | `pre-install` (핸들러에서 예외를 던짐) |
| 모든 업그레이드 시 상태 조정 실행 | `shouldRunOnVersionUpgrade: true`를 사용하는 `post-install` |
| 최초 설치에서만 1회성 설정 수행 | `shouldRunOnVersionUpgrade: false`(기본값)을 사용하는 `post-install` |
<Note>
확신이 서지 않는다면 기본적으로 **post-install**을 사용하세요. 마이그레이션 자체가 파괴적이며 이전 상태가 사라지기 전에 이를 가로채야 할 때에만 pre-install을 사용하세요.
</Note>
</Accordion>
</AccordionGroup>
@@ -0,0 +1,51 @@
---
title: 개요
description: 앱 자체를 구성합니다. 앱의 ID, 기본 권한, 그리고 설치 시점에 무엇이 실행되는지를 설정합니다.
icon: screwdriver-wrench
---
Twenty 앱의 **구성 레이어(config layer)** 는 앱의 ID, 보유한 권한, 설치나 업그레이드 시 실행되는 코드를 포함해, 앱을 *플랫폼에 어떻게 설명할지* 정의합니다. 이 선언들은 새로운 데이터 구조나 런타임 동작을 추가하지 않습니다. 대신 Twenty에게 앱이 무엇인지와 어떻게 설정해야 하는지를 알려줍니다.
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
```
## 이 섹션에서 다루는 내용
<CardGroup cols={2}>
<Card title="애플리케이션 구성" icon="rocket" href="/l/ko/developers/extend/apps/config/application">
`defineApplication` — ID, 기본 역할, 변수, 마켓플레이스 메타데이터를 정의합니다.
</Card>
<Card title="역할 및 권한" icon="shield-halved" href="/l/ko/developers/extend/apps/config/roles">
`defineRole` — 앱의 로직 함수가 무엇을 읽고 쓸 수 있는지 선언합니다.
</Card>
<Card title="설치 훅" icon="wrench" href="/l/ko/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction` 및 `definePostInstallLogicFunction` — 데이터를 백업하고, 기본값을 시드하며, 업그레이드를 검증합니다.
</Card>
</CardGroup>
## 구성 요소 간의 관계
* **애플리케이션**이 진입점입니다. 모든 앱에는 정확히 한 번의 `defineApplication()` 호출이 있으며, 이 호출은 하나의 \*\*역할(Role)\*\*을 기본값으로 가리킵니다.
* \*\*역할(Role)\*\*은 앱의 로직 함수와 프런트 컴포넌트가 무엇을 읽고 쓸 수 있는지를 제어합니다. 최소 권한 원칙을 따르세요. 코드에 실제로 필요한 권한만 부여하세요.
* \*\*설치 훅(Install Hooks)\*\*은 설치나 업그레이드 중에 실행됩니다. 메타데이터 마이그레이션 이전에 실행되는 사전 설치 훅은 위험한 업그레이드를 거부할 수 있고, 마이그레이션 이후에 실행되는 사후 설치 훅은 새 스키마에 맞춰 기본 데이터를 시드할 수 있습니다.
<Note>
설치 훅은 [로직 함수](/l/ko/developers/extend/apps/logic/logic-functions) 런타임을 공유합니다. 동일한 핸들러 시그니처, 동일한 환경 변수, 동일한 타입이 지정된 API 클라이언트를 사용하지만, 자체 define 함수로 선언되고 일반 트리거 모델(HTTP, cron, 데이터베이스 이벤트) 외부에서 동작합니다.
</Note>
@@ -0,0 +1,67 @@
---
title: 퍼블릭 에셋
description: 정적 파일(이미지, 아이콘, 폰트 등)을 public/ 폴더를 통해 앱과 함께 제공합니다.
icon: folder-open
---
앱 루트의 `public/` 폴더에는 정적 파일이 있습니다 — 이미지, 아이콘, 폰트 또는 런타임에 필요한 기타 에셋. 이 파일들은 빌드에 자동 포함되고, 개발 모드에서 동기화되며, 서버로 업로드됩니다.
`public/`에 있는 파일은 다음과 같은 특성을 가집니다:
* **공개적으로 접근 가능** — 서버로 동기화되면 에셋은 공개 URL로 제공됩니다. 접근하려면 인증이 필요하지 않습니다.
* **프런트 컴포넌트에서 사용 가능** — React 컴포넌트 내부에서 이미지, 아이콘 또는 미디어를 표시하기 위해 에셋 URL을 사용하세요.
* **로직 함수에서 사용 가능** — 이메일, API 응답 또는 서버 측 로직에서 에셋 URL을 참조하세요.
* **마켓플레이스 메타데이터에 사용** — `defineApplication()`의 `logoUrl` 및 `screenshots` 필드는 이 폴더의 파일을 참조합니다(예: `public/logo.png`). 앱이 게시되면 이러한 항목이 마켓플레이스에 표시됩니다.
* **개발 모드에서 자동 동기화** — `public/`에 파일을 추가, 업데이트 또는 삭제하면 자동으로 서버와 동기화됩니다. 재시작이 필요 없습니다.
* **빌드에 포함** — `yarn twenty dev:build`는 모든 퍼블릭 에셋을 배포 출력물에 번들합니다.
## `getPublicAssetUrl`로 퍼블릭 에셋에 접근하기
`twenty-sdk`의 `getPublicAssetUrl` 헬퍼를 사용해 `public/` 디렉터리의 파일 전체 URL을 가져오세요. 이는 로직 함수와 프런트 컴포넌트 모두에서 동작합니다.
**로직 함수에서:**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
```
**프런트 컴포넌트에서:**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
`path` 인수는 앱의 `public/` 폴더를 기준으로 합니다. `getPublicAssetUrl('logo.png')`과 `getPublicAssetUrl('public/logo.png')`는 동일한 URL로 해석됩니다 — `public/` 접두사가 있으면 자동으로 제거됩니다.
@@ -0,0 +1,94 @@
---
title: 역할 및 권한
description: 앱의 로직 함수와 프런트 컴포넌트가 어떤 객체와 필드를 읽고 쓸 수 있는지 선언합니다.
icon: shield-halved
---
\*\*역할(role)\*\*은 권한 집합입니다. 즉, 앱이 어떤 객체를 읽거나 쓸 수 있는지, 어떤 필드를 볼 수 있는지, 그리고 어떤 플랫폼 수준 기능을 사용할 수 있는지를 정의합니다. 모든 앱의 로직 함수와 프런트 컴포넌트는 `defineApplicationRole()`로 표시된 역할의 권한을 상속받습니다(아래 [기본 함수 역할](#the-default-function-role)을 참조하세요).
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## 기본 함수 역할
새 앱을 스캐폴딩하면, CLI가 `defineApplicationRole()`로 선언된 기본 역할 파일을 생성합니다:
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()`는 설치 시 애플리케이션의 기본 역할로 사용할 역할을 표시하는, `defineRole()`에 대한 간단한 래퍼입니다. 검증 방식은 `defineRole`과 동일하지만, 빌드 파이프라인이 애플리케이션 매니페스트의 `defaultRoleUniversalIdentifier`에 해당 역할의 `universalIdentifier`를 자동으로 연결하므로, [`defineApplication`](/l/ko/developers/extend/apps/config/application)에서 직접 참조할 필요가 없습니다.
노트:
* 앱당 정확히 **하나의** `defineApplicationRole(...)`만 허용되며, 둘 이상 발견되면 매니페스트 빌드가 실패합니다.
* 앱에서 제공하는 **추가** 역할에는 `defineApplicationRole()`가 아니라 `defineRole()`을 사용하세요.
* 호환성을 위해 `defineApplication()`에서 `defaultRoleUniversalIdentifier`를 명시적으로 설정하는 방식도 여전히 지원되지만, 이제는 `defineApplicationRole()` 방식이 권장되며 이전 방식은 더 이상 권장되지 않습니다.
## 모범 사례
* 스캐폴딩된 역할에서 시작한 다음 점진적으로 권한을 제한하세요. 기본 설정은 광범위한 읽기 액세스를 부여하는데, 이는 프로덕션 환경에서 원하는 경우가 거의 없습니다.
* `objectPermissions`와 `fieldPermissions`를 함수에 실제로 필요한 정확한 객체와 필드로 교체하세요.
* `permissionFlagUniversalIdentifiers`는 플랫폼 수준 기능에 대한 액세스를 제어합니다. 최소한으로 유지하세요.
* 동작 예제를 참조하세요: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
@@ -0,0 +1,50 @@
---
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/` 어디에서든 필드를 감지합니다.
* 표준 페이지 레이아웃(예: 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)를 참조하세요.
@@ -0,0 +1,104 @@
---
title: 개체
description: defineObject를 사용하여 고유한 필드를 가진 사용자 정의 테이블인 새 레코드 유형을 선언합니다.
icon: 테이블
---
사용자 정의 **개체**는 워크스페이스에 앱이 추가하는 새로운 레코드 유형입니다. 엽서(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` 배열은 선택 사항입니다. 사용자 정의 필드 없이도 개체를 정의할 수 있습니다.
* 여기에서 정의된 인라인 필드는 `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>
## 기본값
리터럴 문자열 기본값은 문자열 **내부에서** 작은따옴표로 감싸야 합니다. 즉, `defaultValue: "'Draft'"`처럼 작성해야 하며, `defaultValue: "Draft"`처럼 작성하면 안 됩니다. 그래서 위의 `status` 필드는 `` `'${PostCardStatus.DRAFT}'` ``를 사용합니다.
따옴표로 감싸지 않은 문자열은 레코드가 생성될 때 평가되는 계산형 기본값으로 예약되어 있습니다.
* `'uuid'` — UUID를 생성합니다 (`UUID` 필드용).
* `'now'` — 현재 타임스탬프입니다 (`DATE_TIME` 필드용).
동일한 규칙이 복합 기본값의 문자열 하위 필드(예: `ACTOR` 필드의 `{ source: "'MANUAL'" }`)와 `SELECT`/`MULTI_SELECT` 값에도 적용됩니다. 따옴표로 감싸지 않은 리터럴 문자열 기본값은 앱을 빌드할 때 경고를 발생시킵니다.
## 다음 단계
* **이 개체를 다른 개체와 연결** — 양방향 관계 패턴은 [Relations](/l/ko/developers/extend/apps/data/relations)를 참조하세요.
* **다른 앱의 개체에 필드 추가** — `defineField()`에 대해서는 [Extending Objects](/l/ko/developers/extend/apps/data/extending-objects)를 참조하세요.
* **UI에 이 개체 표시** — 사이드바에 배치하려면 [Views](/l/ko/developers/extend/apps/layout/views) 및 [Navigation Menu Items](/l/ko/developers/extend/apps/layout/navigation-menu-items)를 참조하세요.
@@ -0,0 +1,97 @@
---
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>
</CardGroup>
## 엔터티 한눈에 보기
| 엔터티 | 목적 | 정의 방식 |
| ------ | --------------------------------------------------------------------------- | ------------------------------------------ |
| **객체** | 고유한 필드를 가진 새로운 사용자 정의 레코드 타입(예: PostCard, Invoice) | `defineObject()` |
| **필드** | 객체의 열 독립형 필드를 사용하면 직접 생성하지 않은 객체도 확장할 수 있습니다(예: Company에 `loyaltyTier` 추가). | `defineField()` |
| **관계** | 두 객체 사이의 양방향 연결 — 양쪽 모두 필드로 선언됩니다. | `FieldType.RELATION`을 사용하는 `defineField()` |
| **색인** | 객체 중 하나에 대해 반복적으로 실행되는 쿼리를 빠르게 하기 위한 데이터베이스 색인 | `defineIndex()` |
SDK는 빌드 시 AST 분석을 통해 이를 감지하므로, 파일 구성은 전적으로 여러분에게 달려 있습니다. 권장 컨벤션은 `src/objects/`, `src/fields/`, `src/indexes/`입니다. 안정적인 `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>
@@ -0,0 +1,160 @@
---
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>
## 표준 객체와의 관계 설정
내장 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`) |
## 인라인 관계 필드
[`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
],
});
```
@@ -0,0 +1,101 @@
---
title: 개념
description: Twenty 앱이 작동하는 방식 — 엔티티 모델, 샌드박싱, 설치 라이프사이클.
icon: sitemap
---
Twenty 앱은 사용자 정의 객체, 로직, UI 컴포넌트 및 AI 기능으로 워크스페이스를 확장하는 TypeScript 패키지입니다. 앱은 Twenty 플랫폼에서 완전한 샌드박싱과 권한 제어 하에 실행됩니다.
## 앱 작동 방식
앱은 `twenty-sdk` 패키지의 `defineEntity()` 함수를 사용해 선언된 **엔티티**의 모음입니다. SDK는 빌드 시점에 AST 분석을 통해 이러한 선언을 감지하고, 앱이 워크스페이스에 추가하는 내용을 완전하게 설명한 **매니페스트**를 생성합니다. 이 함수들은 빌드 시점에 구성을 검증하고 IDE 자동 완성과 타입 안정성을 제공합니다.
```
your-app/
├── src/
│ ├── application-config.ts ← defineApplication (required, one per app)
│ ├── roles/ ← defineRole
│ ├── objects/ ← defineObject
│ ├── fields/ ← defineField
│ ├── logic-functions/ ← defineLogicFunction
│ ├── front-components/ ← defineFrontComponent
│ ├── skills/ ← defineSkill
│ ├── agents/ ← defineAgent
│ ├── views/ ← defineView
│ ├── navigation-menu-items/ ← defineNavigationMenuItem
│ └── page-layouts/ ← definePageLayout
├── public/ ← Static assets (images, icons)
└── package.json
```
<Note>
**파일 구성은 사용자의 선택입니다.** 엔티티 감지는 AST 기반이며 — 파일 위치와 관계없이 SDK가 `export default defineEntity(...)` 호출을 찾습니다. 위의 폴더 구조는 관례이며 필수 사항은 아닙니다.
</Note>
## 엔티티 유형
| 엔티티 | 목적 | 문서 |
| --------------- | ------------------------- | ----------------------------------------------------------------------------- |
| **애플리케이션** | 앱 식별, 기본 역할, 변수 | [Application Config](/l/ko/developers/extend/apps/config/application) |
| **역할** | 객체와 필드에 대한 권한 세트 | [Roles & Permissions](/l/ko/developers/extend/apps/config/roles) |
| **객체** | 필드가 있는 사용자 정의 레코드 타입 | [Objects](/l/ko/developers/extend/apps/data/objects) |
| **필드** | 다른 앱의 객체에 필드를 추가 | [Extending Objects](/l/ko/developers/extend/apps/data/extending-objects) |
| **관계** | 객체 간 양방향 링크 | [Relations](/l/ko/developers/extend/apps/data/relations) |
| **로직 함수** | 트리거가 있는 서버 측 TypeScript | [로직 함수](/l/ko/developers/extend/apps/logic/logic-functions) |
| **스킬** | 재사용 가능한 AI 에이전트 지침 | [스킬 및 에이전트](/l/ko/developers/extend/apps/logic/skills-and-agents) |
| **에이전트** | 사용자 지정 프롬프트를 사용하는 AI 에이전트 | [스킬 및 에이전트](/l/ko/developers/extend/apps/logic/skills-and-agents) |
| **연결 제공자** | 서드파티 API용 OAuth 자격 증명 | [Connections](/l/ko/developers/extend/apps/logic/connections) |
| **뷰** | 사전 구성된 레코드 목록 뷰 | [Views](/l/ko/developers/extend/apps/layout/views) |
| **내비게이션 메뉴 항목** | 사용자 정의 사이드바 항목 | [Navigation Menu Items](/l/ko/developers/extend/apps/layout/navigation-menu-items) |
| **페이지 레이아웃** | 레코드 상세 페이지의 탭과 위젯 | [Page Layouts](/l/ko/developers/extend/apps/layout/page-layouts) |
| **프런트 컴포넌트** | Twenty 내부의 샌드박스 React UI | [프런트 컴포넌트](/l/ko/developers/extend/apps/layout/front-components) |
| **명령 메뉴 항목** | 빠른 작업 및 Cmd+K 항목 | [Command Menu Items](/l/ko/developers/extend/apps/layout/command-menu-items) |
## 샌드박싱
* **로직 함수**는 서버의 격리된 Node.js 프로세스에서 실행됩니다. 이들은 앱의 역할 권한 범위로 제한된 타입이 지정된 API 클라이언트를 통해서만 데이터에 접근합니다.
* **프런트 컴포넌트**는 Remote DOM을 사용하는 Web Worker에서 실행됩니다 — 메인 페이지와는 샌드박스로 격리되어 있지만 네이티브 DOM 요소를 렌더링합니다(iframe이 아님). 이들은 메시지 전달 호스트 API를 통해 Twenty와 통신합니다.
* **권한**은 API 수준에서 강제 적용됩니다. 런타임 토큰(`TWENTY_APP_ACCESS_TOKEN`)은 `defineApplication()`에 정의된 역할에서 파생됩니다.
## 앱 라이프사이클
```
┌─────────────────────────────────────────────────────────┐
│ Development │
│ npx create-twenty-app → yarn twenty dev (live sync) │
├─────────────────────────────────────────────────────────┤
│ Build & Deploy │
│ yarn twenty dev:build → yarn twenty app:publish │
├─────────────────────────────────────────────────────────┤
│ Install flow │
│ upload → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
├─────────────────────────────────────────────────────────┤
│ Publish │
│ npm publish → appears in Twenty marketplace │
└─────────────────────────────────────────────────────────┘
```
* **`yarn twenty dev`** — 소스 파일을 감시하고 연결된 Twenty 서버로 변경 사항을 실시간 동기화합니다. 스키마가 변경되면 타입이 지정된 API 클라이언트가 자동으로 재생성됩니다.
* **`yarn twenty dev:build`** — TypeScript를 컴파일하고, 로직 함수와 프런트 컴포넌트를 esbuild로 번들링하며, 매니페스트를 생성합니다.
* **사전/사후 설치 훅** — 설치 중에 실행되는 선택적 함수입니다. 자세한 내용은 [Install Hooks](/l/ko/developers/extend/apps/config/install-hooks)를 참조하세요.
## 다음 단계
<CardGroup cols={2}>
<Card title="설정" icon="screwdriver-wrench" href="/l/ko/developers/extend/apps/config/overview">
애플리케이션 식별, 기본 역할, 설치 훅.
</Card>
<Card title="데이터" icon="database" href="/l/ko/developers/extend/apps/data/overview">
객체, 필드, 양방향 관계.
</Card>
<Card title="로직" icon="bolt" href="/l/ko/developers/extend/apps/logic/overview">
로직 함수, 스킬, 에이전트, OAuth 연결.
</Card>
<Card title="레이아웃" icon="table-columns" href="/l/ko/developers/extend/apps/layout/overview">
뷰, 내비게이션, 페이지 레이아웃, 프런트 컴포넌트.
</Card>
<Card title="작업" icon="rocket" href="/l/ko/developers/extend/apps/operations/overview">
CLI, 테스트, 리모트, CI, 앱 게시.
</Card>
</CardGroup>
@@ -0,0 +1,87 @@
---
title: 로컬 서버
description: 로컬 Twenty Docker 서버를 관리합니다. 시작, 중지, 업그레이드, 병렬 테스트 인스턴스 및 수동 SDK 설정을 수행할 수 있습니다.
icon: server
---
## 로컬 서버 관리
로컬 Twenty 컨테이너를 제어하려면 `yarn twenty docker:*`를 사용하세요:
| 명령 | 하는 일 |
| -------------------------------------- | ----------------------------- |
| `yarn twenty docker:start` | 서버 시작(필요하면 이미지를 가져옴) |
| `yarn twenty docker:start 2.2.0` | 특정 서버 버전을 시작합니다 |
| `yarn twenty docker:start --port 3030` | 사용자 지정 포트에서 시작 |
| `yarn twenty docker:stop` | 서버 중지(데이터 보존) |
| `yarn twenty docker:status` | URL, 버전 및 로그인 자격 증명 표시 |
| `yarn twenty docker:logs` | 서버 로그 스트리밍 |
| `yarn twenty docker:reset` | 데이터를 모두 삭제하고 새로 시작 |
| `yarn twenty docker:upgrade` | `twenty-app-dev` 최신 이미지를 가져오기 |
| `yarn twenty docker:upgrade 2.2.0` | 특정 버전으로 업그레이드합니다 |
데이터는 두 개의 Docker 볼륨에 저장되어 재시작 후에도 유지됩니다(PostgreSQL은 `twenty-app-dev-data`, 파일은 `twenty-app-dev-storage`). `reset`을 사용하여 모든 것을 삭제하세요.
## 서버 버전 고정하기
버전을 전달하지 않으면, `docker:start`는 `package.json`의 앱 `engines.twenty` 범위에서 버전을 결정합니다. 이 범위는 앱이 설치될 때 서버가 검증에 사용하는 범위와 같습니다. 이 명령은 해당 범위를 만족하는 가장 최근에 게시된 `twenty-app-dev` 이미지를 시작하며, 해당 필드가 없거나 게시된 버전이 범위와 일치하지 않으면 `latest`로 대체합니다:
```json filename="package.json"
{
"engines": {
"twenty": ">=2.2.0"
}
}
```
단일 실행에서만 범위를 재정의하려면 버전을 명시적으로 전달하세요: `yarn twenty docker:start 2.3.0`. 이미 다른 버전에서 컨테이너가 존재하는 경우, `docker:start`는 해당 컨테이너를 제자리에서 업그레이드합니다(데이터 볼륨을 유지하면서 컨테이너를 재생성).
## 서버 이미지 업그레이드
`yarn twenty docker:upgrade`는 최신 이미지를 가져와 다이제스트를 비교하고, 실제로 변경 사항이 있을 때만 컨테이너를 다시 생성합니다. 데이터 볼륨은 유지되며 — 컨테이너만 교체됩니다. 새 이미지를 가져왔고 컨테이너가 실행 중이었다면, 업그레이드는 자동으로 새 컨테이너를 시작합니다; 이후 `yarn twenty docker:start`를 실행하여 상태가 정상(healthy)이 될 때까지 기다리세요.
```bash filename="Terminal"
yarn twenty docker:upgrade # Latest
yarn twenty docker:upgrade 2.2.0 # Specific version
```
`yarn twenty docker:status`로 실행 중인 버전을 확인하세요(컨테이너에 포함된 `APP_VERSION`을 표시합니다).
## 병렬 테스트 인스턴스 실행
완전히 분리된 두 번째 인스턴스를 관리하려면 모든 `docker:*` 명령에 `--test`를 전달하세요 — 메인 개발 데이터를 건드리지 않고 통합 테스트를 실행하거나 실험할 때 유용합니다:
| 명령 | 하는 일 |
| ----------------------------------- | ---------------------------- |
| `yarn twenty docker:start --test` | 테스트 인스턴스를 시작합니다(기본 포트는 2021) |
| `yarn twenty docker:stop --test` | 중지 |
| `yarn twenty docker:status --test` | 상태 표시 |
| `yarn twenty docker:logs --test` | 로그 스트리밍 |
| `yarn twenty docker:reset --test` | 데이터 삭제 |
| `yarn twenty docker:upgrade --test` | 이미지 업그레이드 |
테스트 인스턴스는 자체 컨테이너(`twenty-app-dev-test`), 전용 볼륨(`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), 구성으로 실행되며, 메인 인스턴스와 충돌 없이 나란히 실행됩니다. `--test`를 `--port`와 함께 사용하여 2021을 재정의하세요.
## 수동 설정(스캐폴더 없이)
기존 프로젝트에 SDK를 추가하는 경우 스캐폴더 단계를 건너뜁니다:
```bash filename="Terminal"
yarn add twenty-sdk twenty-client-sdk
```
`package.json`에 스크립트를 추가하세요:
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
이제 `yarn twenty dev`, `yarn twenty docker:start` 등을 실행할 수 있습니다.
<Note>
`twenty-sdk`를 전역으로 설치하지 마세요 — 각 프로젝트에 고정하여 각 앱이 자체 버전을 사용하도록 하세요.
</Note>
@@ -0,0 +1,61 @@
---
title: 프로젝트 구조
description: 스캐폴딩된 Twenty 앱 안에 무엇이 들어 있는지 — 파일, 폴더, 그리고 각 요소의 역할을 설명합니다.
icon: folder-tree
---
`npx create-twenty-app`으로 생성한 새 앱은 다음과 같은 구조입니다:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
src/
application-config.ts # Required — your app's entry point
default-role.ts # Permissions for logic functions
constants/
universal-identifiers.ts # Auto-generated UUIDs and metadata
__tests__/
setup-test.ts
app-install.integration-test.ts
.github/workflows/ci.yml # GitHub Actions
public/ # Static assets
vitest.config.ts # Test runner config
tsconfig.json, tsconfig.spec.json
.nvmrc, .yarnrc.yml, .oxlintrc.json
README.md, LLMS.md
```
## 주요 파일
| 파일 / 폴더 | 목적 |
| ---------------------------------------- | -------------------------------- |
| `src/application-config.ts` | **필수.** 앱의 기본 구성 파일입니다. |
| `src/default-role.ts` | 로직 함수가 접근할 수 있는 범위를 제어하는 기본 역할 |
| `src/constants/universal-identifiers.ts` | 자동 생성된 UUID와 앱 메타데이터(표시 이름, 설명). |
| `src/__tests__/` | 통합 테스트(설정 + 예제 테스트). |
| `public/` | 앱과 함께 제공되는 정적 에셋(이미지, 폰트). |
<Note>
**파일 구성은 사용자의 선택입니다.** 위 폴더들은 관례일 뿐이며, SDK는 파일 위치와 관계없이 `export default defineEntity(...)` 호출에 대한 AST 분석을 통해 엔티티를 감지합니다.
</Note>
## 의존성
두 Twenty SDK 패키지는 `dependencies`가 아니라 `devDependencies` 아래에 속해야 합니다.
```json filename="package.json"
{
"dependencies": {},
"devDependencies": {
"twenty-client-sdk": "^2.13.0",
"twenty-sdk": "^2.13.0"
}
}
```
* \*\*`twenty-sdk`\*\*는 `twenty` CLI와 빌드/스캐폴딩 도구를 제공합니다. 이 패키지는 개발 및 빌드 시점에만 실행되며, 배포된 앱의 런타임에서는 전혀 임포트되지 않습니다.
* \*\*`twenty-client-sdk`\*\*는 앱 코드(`CoreApiClient`, `MetadataApiClient`, `RestApiClient`)에서 임포트되지만, 런타임에는 Twenty가 이를 제공합니다. 로직 함수는 생성된 SDK 레이어에서 이를 가져오고, 프런트엔드 컴포넌트는 서버에서 제공되는 모듈에서 이를 해석하여 가져옵니다. 설치된 사본은 타입 검사와 배포 시점 빌드에만 사용되므로, 배포된 번들에 포함되어 함께 제공될 필요가 없습니다.
어느 한 패키지라도 `dependencies` 아래에 두면, 설치된 앱의 런타임 번들에 포함되어 쓸모없는 부하가 됩니다. `twenty build`는 둘 중 하나라도 여전히 `dependencies` 아래에 나열되어 있으면 경고를 출력합니다.
앱의 실제 런타임 의존성(로직 함수가 런타임에 실제로 임포트하는 라이브러리)은 평소와 같이 `dependencies` 아래에 추가하세요.
@@ -0,0 +1,176 @@
---
title: 빠른 시작
icon: rocket
description: 몇 분 만에 첫 번째 Twenty 앱을 만들어 보세요.
---
## 사전 준비
* **Node.js 24+** — [여기에서 다운로드](https://nodejs.org/)
* **Yarn 4** — Corepack을 통해 Node.js와 함께 제공됩니다. 활성화하려면: `corepack enable`
* **Docker** — [여기에서 다운로드](https://www.docker.com/products/docker-desktop/). 로컬 Twenty 서버를 실행하려면 필요합니다. 이미 다른 곳에서 Twenty가 실행 중이라면 건너뛰세요.
Twenty 앱을 빌드하는 과정은 세 단계로 이루어집니다. 스캐폴더는 이를 단일 해피 패스 명령으로 합쳐 주지만, 각 단계는 별개의 개념입니다 — 문제가 발생했을 때 현재 단계가 어디인지 알면 무엇을 고쳐야 하는지 파악할 수 있습니다.
| 단계 | 하는 일 | 도구 | 결과 |
| ------------ | ------------------------- | ----------------------------- | -------------------- |
| **1. 스캐폴딩** | 앱의 소스 코드를 생성 | `npx create-twenty-app` | 디스크에 TypeScript 프로젝트 |
| **2. 서버 실행** | 동기화 대상으로 사용할 Twenty 서버 시작 | Docker + `yarn twenty server` | 실행 중인 Twenty 인스턴스 |
| **3. 동기화** | 코드를 서버와 실시간 동기화 | `yarn twenty dev` | 변경 사항이 UI에 표시됨 |
---
## 1단계 — 프로젝트 스캐폴딩
템플릿에서 새 앱을 생성합니다:
```bash filename="Terminal"
npx create-twenty-app@latest my-twenty-app
```
이름과 설명을 묻는 프롬프트가 표시됩니다 — 기본값을 사용하려면 **Enter**를 누르세요. 이 명령은 `my-twenty-app/`에 시작용 `application-config.ts`, 기본 역할, CI 워크플로, 통합 테스트가 포함된 TypeScript 프로젝트를 생성합니다.
**이 단계를 마치면:** 로컬 머신에 앱의 소스 코드가 준비됩니다. 아직 실행되지는 않았습니다 — 그건 2단계에서 진행합니다.
---
## 2단계 — 로컬 Twenty 서버 실행
앱은 동기화할 Twenty 서버가 필요합니다. 이 서버는 Docker에서 로컬로 실행되는 완전한 Twenty 인스턴스입니다 — UI, GraphQL API, PostgreSQL을 포함합니다. 로컬 코드가 해당 서버로 정의를 업로드하면 UI에 표시됩니다.
스캐폴더가 서버 시작 여부를 묻습니다:
> **로컬 Twenty 인스턴스를 설정하시겠습니까?**
* **Yes(권장)** — `twentycrm/twenty-app-dev` Docker 이미지를 가져와 포트 `2020`에서 시작합니다. 먼저 Docker가 실행 중인지 확인하세요.
* **No** — 이미 연결하려는 Twenty 서버가 있는 경우 선택하세요. `yarn twenty remote:add`로 나중에 연결할 수 있습니다.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="로컬 인스턴스를 시작할까요?" />
</div>
서버가 올라오면 로그인할 수 있도록 브라우저가 열립니다. 미리 준비된 데모 계정을 사용하세요:
* **이메일:** `tim@apple.dev`
* **비밀번호:** `tim@apple.dev`
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty 로그인 화면" />
</div>
다음 화면에서 **Authorize**를 클릭하세요 — 그러면 CLI가 워크스페이스에 접근할 수 있게 됩니다.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty CLI 권한 부여 화면" />
</div>
권한 부여가 완료되면 터미널에 설정이 완료되었다는 메시지가 표시됩니다.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="앱 스캐폴딩 성공" />
</div>
**이 단계를 마치면:** [http://localhost:2020](http://localhost:2020)에서 Twenty 서버가 실행 중이며, CLI가 해당 서버로 동기화하도록 권한이 부여됩니다.
<Note>
Docker가 설치되어 있지 않거나 실행 중이 아니면, 스캐폴더가 OS에 맞는 올바른 시작 명령을 알려줍니다. Docker가 올라오면 `yarn twenty docker:start`로 이어서 진행할 수 있습니다 — 다시 스캐폴딩할 필요는 없습니다.
</Note>
---
## 3단계 — 변경 사항 동기화
이 단계는 대부분의 시간을 보내게 될 내부 루프입니다.
```bash filename="Terminal"
cd my-twenty-app
yarn twenty dev
```
`src/`를 감시하며 변경될 때마다 다시 빌드하고, 결과를 서버에 동기화합니다. 파일을 수정해 저장하면 몇 초 안에 서버에 변경 사항이 반영됩니다. 터미널에서 실시간 상태 패널을 확인할 수 있습니다.
더 자세한 출력(빌드 로그, 동기화 요청, 오류 트레이스)을 보려면 `--verbose`를 추가하세요.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.png" alt="개발 모드 터미널 출력" />
</div>
[http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer)를 엽니다. **Your Apps** 아래에 앱이 표시됩니다.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Your Apps 목록에 My twenty app이 표시됨" />
</div>
**My twenty app**을 클릭하여 **애플리케이션 등록**을 확인하세요 — 앱을 설명하는 서버 수준의 레코드입니다(이름, 식별자, OAuth 자격 증명, 소스). 하나의 등록은 동일한 서버의 여러 워크스페이스에 설치할 수 있습니다.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="애플리케이션 등록 세부정보" />
</div>
워크스페이스 설치를 확인하려면 **View installed app**을 클릭하세요. **About** 탭에는 버전과 관리 옵션이 표시됩니다.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="설치된 앱" />
</div>
**이 단계를 마치면:** 라이브 개발 루프가 준비됩니다. `src/`의 파일을 수정하면 UI에 반영됩니다.
### CI 및 스크립트를 위한 1회성 동기화
`--once`를 전달하면 한 번만 빌드 + 동기화를 실행하고 종료합니다 — 파이프라인은 동일하고, 워처는 없습니다:
```bash filename="Terminal"
yarn twenty dev --once
```
| 명령 | 동작 | 사용 시점 |
| ---------------------------------- | ------------------------------------------------ | -------------------------------------- |
| `yarn twenty dev` | 소스 파일을 감시하고 변경될 때마다 다시 동기화합니다. 중지할 때까지 계속 실행됩니다. | 대화형 로컬 개발. |
| `yarn twenty dev --once` | 한 번만 빌드 + 동기화를 수행하고, 성공 시 `0`, 실패 시 `1`로 종료합니다. | CI, pre-commit 훅, AI 에이전트, 스크립트형 워크플로. |
| `yarn twenty dev --once --dry-run` | 메타데이터 변경 사항을 **실제로 적용하지 않고** 빌드하고 출력합니다. | 커밋하기 전에 동기화가 어떤 변경을 수행할지 살펴봅니다. |
두 모드 모두 인증된 리모트가 필요합니다. `--dry-run`에 대한 자세한 내용은 [동기화 및 복구](/l/ko/developers/extend/apps/operations/sync-and-recovery#previewing-changes-dry-run)를 참고하세요.
### 개발 모드 옵션
| 플래그 | 설명 |
| ------------------------------------- | -------------------------------------------------------------------- |
| `--once` | 한 번만 빌드하고 동기화한 다음 종료합니다. |
| `--dry-run` | `--once`를 사용하면 메타데이터 변경 사항을 실제로 적용하지 않고 미리 볼 수 있습니다. 아무것도 기록하지 않습니다. |
| `--debounceMs \<ms>` | 파일 변경 디바운스 지연 시간을 밀리초 단위로 설정합니다(기본값: `2000`). |
| `--verbose` / `--debug` | 자세한 빌드 로그, 동기화 요청, 오류 추적을 표시합니다. |
## 만들 수 있는 것
앱은 **엔터티**로 구성됩니다 — 각 엔터티는 하나의 `export default`만 포함하는 TypeScript 파일로 정의됩니다:
| 엔터티 | 하는 일 |
| ------------- | ---------------------------------------------------------- |
| **객체 & 필드** | 사용자 정의 데이터 모델(Post Card, Invoice 등) 타입이 지정된 필드 포함 |
| **로직 함수** | HTTP 라우트, cron 스케줄 또는 데이터베이스 이벤트로 트리거되는 서버 측 TypeScript 코드 |
| **프론트 컴포넌트** | Twenty의 UI(사이드 패널, 위젯, 명령 메뉴) 안에 렌더링되는 React 컴포넌트 |
| **스킬 & 에이전트** | AI 기능 — 재사용 가능한 지침과 자율형 어시스턴트 |
| **뷰 & 내비게이션** | 사전 구성된 목록 뷰와 사이드바 메뉴 항목 |
| **페이지 레이아웃** | 탭과 위젯이 있는 사용자 정의 레코드 상세 페이지 |
전체 참고 문서: [개념](/l/ko/developers/extend/apps/getting-started/concepts).
## 다음 단계
<CardGroup cols={2}>
<Card title="설정" icon="screwdriver-wrench" href="/l/ko/developers/extend/apps/config/overview">
애플리케이션 ID, 기본 역할, 설치 훅, 공개 자산.
</Card>
<Card title="데이터" icon="database" href="/l/ko/developers/extend/apps/data/overview">
객체, 필드, 그리고 양방향 관계.
</Card>
<Card title="로직" icon="bolt" href="/l/ko/developers/extend/apps/logic/overview">
로직 함수, 스킬, 에이전트, OAuth 연결.
</Card>
<Card title="레이아웃" icon="table-columns" href="/l/ko/developers/extend/apps/layout/overview">
뷰, 내비게이션, 페이지 레이아웃, 프론트 컴포넌트.
</Card>
<Card title="작업" icon="rocket" href="/l/ko/developers/extend/apps/operations/overview">
CLI, 테스트, 리모트, CI, 그리고 앱 게시.
</Card>
</CardGroup>
@@ -0,0 +1,58 @@
---
title: 스캐폴딩
description: "`yarn twenty dev:add`를 사용해 엔티티 파일을 대화형으로 생성하여 객체, 필드, 뷰, 로직 함수 등을 추가하세요."
icon: wand-magic-sparkles
---
엔티티 파일을 수동으로 만드는 대신, 대화형 스캐폴더를 사용하세요:
```bash filename="Terminal"
yarn twenty dev:add
```
이 도구는 엔티티 유형을 선택하도록 안내하고, 필요한 필드를 하나씩 진행한 다음, 안정적인 `universalIdentifier`와 올바른 `defineEntity()` 호출이 포함된 바로 사용할 수 있는 파일을 생성합니다.
첫 번째 프롬프트를 건너뛰려면 엔티티 타입을 직접 전달할 수도 있습니다:
```bash filename="Terminal"
yarn twenty dev:add object
yarn twenty dev:add logicFunction
yarn twenty dev:add frontComponent
```
## 사용 가능한 엔티티 타입
| 엔티티 유형 | 명령 | 생성된 파일 |
| ----------- | ---------------------------------------- | ------------------------------------------------------- |
| 객체 | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
| 필드 | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
| 로직 함수 | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
| 프런트 컴포넌트 | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
| 역할 | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
| 스킬 | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
| 에이전트 | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
| 뷰 | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
| 내비게이션 메뉴 항목 | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
| 페이지 레이아웃 | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
## 스캐폴더가 생성하는 것
각 엔티티 타입은 자체 템플릿을 가집니다. 예를 들어, `yarn twenty dev:add object`는 다음을 묻습니다:
1. **이름(단수)** — 예: `invoice`
2. **이름(복수)** — 예: `invoices`
3. **레이블(단수)** — 이름에서 자동으로 채워짐(예: `Invoice`)
4. **레이블(복수)** — 자동으로 채워짐(예: `Invoices`)
5. **뷰와 내비게이션 항목을 생성할까요?** — 예라고 답하면, 스캐폴더가 새 객체에 대한 일치하는 뷰와 사이드바 링크도 생성합니다.
다른 엔티티 타입은 더 단순한 프롬프트를 사용하며 — 대부분 이름만 묻습니다.
`field` 엔티티 타입은 더 자세합니다: 필드 이름, 레이블, 타입( `TEXT`, `NUMBER`, `SELECT`, `RELATION` 등 사용 가능한 모든 필드 타입 목록에서 선택), 그리고 대상 객체의 `universalIdentifier`를 묻습니다.
## 사용자 지정 출력 경로
`--path` 플래그를 사용해 생성된 파일을 사용자 지정 위치에 배치하세요:
```bash filename="Terminal"
yarn twenty dev:add logicFunction --path src/custom-folder
```
@@ -0,0 +1,14 @@
---
title: 문제 해결
description: 일반적인 첫 실행 문제 — Docker, Node 버전, Yarn, 의존성.
icon: wrench
---
* **Docker 오류** — `yarn twenty docker:start`를 실행하기 전에 Docker Desktop(또는 데몬)이 실행 중인지 확인하세요. 오류 메시지에 OS에 맞는 올바른 시작 명령이 표시됩니다.
* **Node 버전 오류** — 24 이상 필요. `node -v`로 확인하세요.
* **Yarn 4 누락** — `corepack enable`을 실행하세요.
* **의존성 문제** — `rm -rf node_modules && yarn install`.
* **`twenty-sdk` v2.8.0으로 업그레이드한 후 오류 발생** — v2.8.0에서 `dependencies`에서 `devDependencies`로 이동했습니다. [프로젝트 구조 → Dependencies](/l/ko/developers/extend/apps/getting-started/project-structure#dependencies)를 참조하세요.
* **`twenty build`는 `dependencies` 아래의 `twenty-client-sdk`에 대해 경고합니다** — 이는 실행 시 Twenty에서 제공되므로, `twenty-sdk`와 함께 `devDependencies`로 옮겨야 합니다. [프로젝트 구조 → Dependencies](/l/ko/developers/extend/apps/getting-started/project-structure#dependencies)를 참조하세요.
막히셨나요? [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322)에서 문의하세요.
@@ -0,0 +1,148 @@
---
title: 명령 메뉴 항목
description: 프런트 컴포넌트를 빠른 작업 및 명령 메뉴(Cmd+K) 항목으로 노출하려면 `defineCommandMenuItem`을 사용합니다.
icon: 터미널
---
**명령 메뉴 항목**은 사용자와 [프런트 컴포넌트](/l/ko/developers/extend/apps/layout/front-components) 사이를 연결하는 다리입니다. 이 항목은 Twenty의 명령 메뉴(Cmd+K)에 구성 요소를 등록하고, 선택적으로 페이지 오른쪽 상단 모서리에 고정된 빠른 작업 버튼으로도 등록합니다.
```ts src/command-menu-items/open-dashboard.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Open Dashboard',
shortLabel: 'Dashboard',
icon: 'IconLayoutDashboard',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
## 구성 필드
| 필드 | 필수 | 설명 |
| --------------------------------------- | --- | ---------------------------------------------------------------------------------------------------------- |
| `universalIdentifier` | 예 | 명령의 안정적인 고유 ID |
| `label` | 예 | 명령 메뉴(Cmd+K)에 표시되는 전체 레이블 |
| `frontComponentUniversalIdentifier` | 예 | 이 명령으로 열리는 프런트 컴포넌트의 `universalIdentifier` |
| `shortLabel` | 아니요 | 고정된 빠른 작업 버튼에 표시되는 더 짧은 레이블 |
| `icon` | 아니요 | 레이블 옆에 표시되는 아이콘 이름(예: `'IconBolt'`, `'IconSend'`) |
| `isPinned` | 아니요 | `true`이면 페이지 우측 상단에 빠른 작업 버튼으로 명령을 표시합니다 |
| `availabilityType` | 아니요 | 명령이 표시되는 위치를 제어합니다: 'GLOBAL'(항상 사용 가능), 'RECORD_SELECTION'(레코드가 선택된 경우에만), 'FALLBACK'(다른 명령이 일치하지 않을 때 표시) |
| `availabilityObjectUniversalIdentifier` | 아니요 | 명령을 특정 객체 타입의 페이지로 제한합니다(예: Company 레코드에서만) |
| `conditionalAvailabilityExpression` | 아니요 | 표시 여부를 동적으로 제어하는 불리언 표현식(아래 참조) |
## 헤드리스 명령
[헤드리스 프런트 컴포넌트](/l/ko/developers/extend/apps/layout/front-components#headless-vs-non-headless)와 짝을 이룬 명령 메뉴 항목은 원클릭 작업—코드 실행, 이동, 확인 후 실행—을 제공하는 가장 전형적인 방식입니다. 프런트 컴포넌트 페이지에서는 동작 후 언마운트 패턴을 처리하는 [SDK Command 구성 요소](/l/ko/developers/extend/apps/layout/front-components#sdk-command-components) (`Command`, `CommandLink`, `CommandModal`, `CommandOpenSidePanelPage`)를 다룹니다.
일반적인 흐름:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
## 조건부 가용성 표현식
`conditionalAvailabilityExpression` 필드를 사용하면 현재 페이지 컨텍스트에 따라 명령의 표시 시점을 제어할 수 있습니다. 표현식을 만들기 위해 `twenty-sdk`에서 타입이 지정된 변수와 연산자를 임포트하세요:
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import {
defineCommandMenuItem,
objectPermissions,
everyEquals,
} from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '...',
label: 'Bulk Update',
availabilityType: 'RECORD_SELECTION',
frontComponentUniversalIdentifier: '...',
conditionalAvailabilityExpression: everyEquals(
objectPermissions,
'canUpdateObjectRecords',
true,
),
});
```
<Note>
`RECORD_SELECTION`은 이미 비어 있지 않은 선택을 의미하므로, 특정 개수를 나타낼 때만(예: `>= 2`) `numberOfSelectedRecords`를 사용하세요.
</Note>
### 컨텍스트 변수
이는 현재 페이지의 상태를 나타냅니다:
| 변수 | 유형 | 설명 |
| ------------------------------ | --------- | ------------------------------------------------- |
| `pageType` | `string` | 현재 페이지 타입(예: 'RecordIndexPage', 'RecordShowPage') |
| `isInSidePanel` | `boolean` | 컴포넌트가 사이드 패널에서 렌더링되는지 여부 |
| `numberOfSelectedRecords` | `number` | 현재 선택된 레코드 수 |
| `isSelectAll` | `boolean` | "전체 선택"이 활성화되었는지 여부 |
| `selectedRecords` | `array` | 선택된 레코드 객체 |
| `favoriteRecordIds` | `array` | 즐겨찾기된 레코드의 ID들 |
| `objectPermissions` | `object` | 현재 객체 타입에 대한 권한 |
| `targetObjectReadPermissions` | `object` | 대상 객체에 대한 읽기 권한 |
| `targetObjectWritePermissions` | `object` | 대상 객체에 대한 쓰기 권한 |
| `featureFlags` | `object` | 활성화된 기능 플래그 |
| `objectMetadataItem` | `object` | 현재 객체 타입의 메타데이터 |
| `hasAnySoftDeleteFilterOnView` | `boolean` | 현재 뷰에 소프트 삭제 필터가 있는지 여부 |
### 연산자
변수를 결합해 불리언 표현식을 만듭니다:
| 연산자 | 설명 |
| ----------------------------------- | --------------------------------------- |
| `isDefined(value)` | 값이 null/undefined가 아니면 `true` |
| `isNonEmptyString(value)` | 값이 비어 있지 않은 문자열이면 `true` |
| `includes(array, value)` | 배열에 해당 값이 포함되어 있으면 `true` |
| `includesEvery(array, prop, value)` | 모든 항목의 속성에 해당 값이 포함되어 있으면 `true` |
| `every(array, prop)` | 모든 항목에서 해당 속성이 truthy이면 `true` |
| `everyDefined(array, prop)` | 모든 항목에서 해당 속성이 정의되어 있으면 `true` |
| `everyEquals(array, prop, value)` | 모든 항목에서 해당 속성이 그 값과 같으면 `true` |
| `some(array, prop)` | 적어도 한 항목에서 해당 속성이 truthy이면 `true` |
| `someDefined(array, prop)` | 적어도 한 항목에서 해당 속성이 정의되어 있으면 `true` |
| `someEquals(array, prop, value)` | 적어도 한 항목에서 해당 속성이 그 값과 같으면 `true` |
| `someNonEmptyString(array, prop)` | 적어도 한 항목에서 해당 속성이 비어 있지 않은 문자열이면 `true` |
| `none(array, prop)` | 모든 항목에서 해당 속성이 falsy이면 `true` |
| `noneDefined(array, prop)` | 모든 항목에서 해당 속성이 정의되지 않았으면 `true` |
| `noneEquals(array, prop, value)` | 어떤 항목에서도 해당 속성이 그 값과 같지 않으면 `true` |
@@ -0,0 +1,545 @@
---
title: 프런트 컴포넌트
description: Twenty의 UI 내부에서 샌드박스 환경으로 격리된 채 렌더링되는 React 컴포넌트를 빌드하세요.
icon: window-maximize
---
프런트 컴포넌트는 Twenty의 UI 내부에서 직접 렌더링되는 React 컴포넌트입니다. 이들은 Remote DOM을 사용하는 격리된 Web Worker에서 실행됩니다 — 코드는 샌드박스 처리되지만 iframe이 아닌 페이지 내에서 네이티브로 렌더링됩니다.
## 프런트 컴포넌트를 사용할 수 있는 위치
프런트 컴포넌트는 Twenty 내에서 두 위치에 렌더링될 수 있습니다:
* **사이드 패널** — 비헤드리스 프런트 컴포넌트는 오른쪽 사이드 패널에서 열립니다. 이는 명령 메뉴에서 프런트 컴포넌트를 트리거할 때의 기본 동작입니다.
* **위젯(대시보드 및 레코드 페이지)** — 프런트 컴포넌트를 [페이지 레이아웃](/l/ko/developers/extend/apps/layout/page-layouts) 내 위젯으로 삽입할 수 있습니다. 대시보드 또는 레코드 페이지 레이아웃을 구성할 때 사용자는 프런트 컴포넌트 위젯을 추가할 수 있습니다.
프런트 컴포넌트만으로는 UI에서 직접 접근할 수 없으므로 *표시*해야 합니다. 이를 수행하는 두 가지 방법은 다음과 같습니다.
* **[명령 메뉴 항목](/l/ko/developers/extend/apps/layout/command-menu-items)과 연결** — 명령 메뉴(Cmd+K)에 등록하고, 선택적으로 고정된 빠른 작업으로 등록합니다.
* **[페이지 레이아웃](/l/ko/developers/extend/apps/layout/page-layouts)에 위젯으로 포함** — 레코드 상세 페이지 또는 대시보드에 배치합니다.
## 기본 예제
프런트 컴포넌트가 실제로 동작하는 모습을 가장 빨리 확인하는 방법은 [`defineCommandMenuItem`](/l/ko/developers/extend/apps/layout/command-menu-items)과 연결하여 페이지 오른쪽 상단에 빠른 작업 버튼으로 표시되게 하는 것입니다.
```tsx src/front-components/hello-world.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
const HelloWorld = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Hello from my app!</h1>
<p>This component renders inside Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
name: 'hello-world',
description: 'A simple front component',
component: HelloWorld,
});
```
```ts src/command-menu-items/hello-world.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
shortLabel: 'Hello',
label: 'Hello World',
icon: 'IconBolt',
isPinned: true,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
});
```
`yarn twenty dev`로 동기화한 후(또는 일회성으로 `yarn twenty dev --once`를 실행한 경우), 페이지 우측 상단에 빠른 작업이 표시됩니다:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="우측 상단의 빠른 작업 버튼" />
</div>
클릭하면 컴포넌트를 인라인으로 렌더링합니다.
## 구성 필드
| 필드 | 필수 | 설명 |
| --------------------- | --- | -------------------------------------- |
| `universalIdentifier` | 예 | 이 컴포넌트의 안정적인 고유 ID |
| `component` | 예 | React 컴포넌트 함수 |
| `name` | 아니요 | 표시 이름 |
| `description` | 아니요 | 컴포넌트가 수행하는 작업에 대한 설명 |
| `isHeadless` | 아니요 | 컴포넌트에 보이는 UI가 없다면 `true`로 설정하세요(아래 참조) |
## 프런트 컴포넌트를 페이지에 배치하기
명령 외에도, 페이지 레이아웃에 위젯으로 추가하여 레코드 페이지에 프런트 컴포넌트를 직접 임베드할 수 있습니다. 자세한 내용은 [페이지 레이아웃](/l/ko/developers/extend/apps/layout/page-layouts)을 참조하세요.
## 헤드리스 vs 비헤드리스
프런트 컴포넌트는 `isHeadless` 옵션으로 제어되는 두 가지 렌더링 모드를 제공합니다:
**비헤드리스(기본값)** — 컴포넌트가 가시적인 UI를 렌더링합니다. 명령 메뉴에서 트리거되면 사이드 패널에서 열립니다. `isHeadless`가 `false`이거나 생략된 경우의 기본 동작입니다.
**헤드리스 (`isHeadless: true`)** — 컴포넌트가 백그라운드에서 보이지 않게 마운트됩니다. 사이드 패널을 열지 않습니다. 헤드리스 컴포넌트는 로직을 실행한 뒤 스스로 언마운트하는 작업에 맞게 설계되었습니다 — 예: 비동기 작업 실행, 페이지로 이동, 확인 모달 표시. 아래에 설명된 SDK Command 컴포넌트와 자연스럽게 짝을 이룹니다.
```tsx src/front-components/sync-tracker.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
import { useEffect } from 'react';
const SyncTracker = () => {
const recordId = useRecordId();
useEffect(() => {
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
}, [recordId]);
return null;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-tracker',
description: 'Tracks record views silently',
isHeadless: true,
component: SyncTracker,
});
```
컴포넌트가 `null`을 반환하기 때문에, Twenty는 해당 컴포넌트에 대한 컨테이너 렌더링을 생략합니다 — 레이아웃에 빈 공간이 생기지 않습니다. 컴포넌트는 여전히 모든 훅과 호스트 통신 API에 접근할 수 있습니다.
## SDK Command 컴포넌트
`twenty-sdk` 패키지는 헤드리스 프런트 컴포넌트를 위해 설계된 네 가지 Command 헬퍼 컴포넌트를 제공합니다. 각 컴포넌트는 마운트 시 동작을 실행하고, 스낵바 알림을 표시하여 오류를 처리하며, 완료되면 프런트 컴포넌트를 자동으로 언마운트합니다.
`twenty-sdk/command`에서 임포트하세요:
* **`Command`** — `execute` prop을 통해 비동기 콜백을 실행합니다.
* **`CommandLink`** — 앱 경로로 이동합니다. Props: `to`, `params`, `queryParams`, `options`.
* **`CommandModal`** — 확인 모달을 엽니다. 사용자가 확인하면 `execute` 콜백을 실행합니다. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — 특정 사이드 패널 페이지를 엽니다. Props: `page`, `pageTitle`, `pageIcon`.
`Command`를 사용해 명령 메뉴에서 동작을 실행하는 헤드리스 프런트 컴포넌트의 전체 예시는 다음과 같습니다:
```tsx src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
});
```
```ts src/command-menu-items/run-action.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
});
```
그리고 실행 전에 확인을 요청하기 위해 `CommandModal`을 사용하는 예시는 다음과 같습니다:
```tsx src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
});
```
## 로직 함수 호출하기
Front 컴포넌트는 샌드박스된 Web Worker 안에서 브라우저 측에서 실행되고, [logic functions](/l/ko/developers/extend/apps/logic/logic-functions)는 서버 측에서 실행됩니다. 두 요소 사이에는 프로세스 내에서의 직접 호출이 없습니다. 대신, Front 컴포넌트는 HTTP를 통해 로직 함수에 접근합니다.
`httpRouteTriggerSettings`로 선언된 로직 함수는 `${TWENTY_API_URL}/s\<path>`의 `/s/` 엔드포인트 아래에 노출됩니다. 여러분의 Front 컴포넌트는 `twenty-client-sdk/rest`의 `RestApiClient`를 사용해 해당 라우트를 호출하며, Twenty가 워커에 주입하는 `TWENTY_APP_ACCESS_TOKEN`으로 인증합니다.
`RestApiClient`는 바로 이런 용도로 설계되었습니다. 이는 워커 환경에서 `TWENTY_API_URL`과 `TWENTY_APP_ACCESS_TOKEN`을 읽어 `Authorization: Bearer` 헤더를 추가하고, JSON을 직렬화 및 파싱하며, 토큰이나 URL이 없거나 응답이 2xx가 아닐 경우 `RestApiClientError`를 발생시켜, 여러분이 각 컴포넌트마다 이러한 보일러플레이트를 다시 구현하지 않아도 되도록 해 줍니다.
헤드리스 Front 컴포넌트는 `Command` 컴포넌트를 통해 마운트 시점에 호출을 실행한 뒤, 자동으로 언마운트될 수 있습니다:
```tsx src/front-components/sync-prs.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Command } from 'twenty-sdk/command';
import { RestApiClient } from 'twenty-client-sdk/rest';
const SyncPrs = () => {
const execute = async () => {
const client = new RestApiClient();
await client.post('/s/github/fetch-prs', {
owner: 'twentyhq',
repo: 'twenty',
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'sync-prs',
description: 'Triggers the fetch-prs logic function',
isHeadless: true,
component: SyncPrs,
});
```
클라이언트에 전달되는 경로는 라우트의 public 경로이며, 로직 함수의 `httpRouteTriggerSettings.path` 앞에 `/s`가 접두사로 붙습니다. `isAuthRequired: true`를 유지하세요. 클라이언트가 컴포넌트용으로 Twenty가 발행한 앱 액세스 토큰을 제공합니다:
```ts src/logic-functions/fetch-prs.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { owner, repo } = (event.body ?? {}) as { owner: string; repo: string };
// ...fetch from GitHub and persist records...
return { ok: true };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-prs',
handler,
httpRouteTriggerSettings: {
path: '/github/fetch-prs',
httpMethod: 'POST',
isAuthRequired: true,
},
});
```
<Note>
`TWENTY_API_URL`과 `TWENTY_APP_ACCESS_TOKEN`은 자동으로 주입됩니다. 자세한 내용은 [Application variables](#application-variables)을 참고하세요. 비밀 애플리케이션 변수는 Front 컴포넌트에 절대 노출되지 않으므로, API 키 및 기타 민감한 로직은 Front 컴포넌트가 아니라 로직 함수 안에 유지해야 합니다.
</Note>
### RestApiClient 레퍼런스
`RestApiClient`를 `twenty-client-sdk/rest`에서 import하세요. 이는 `CoreApiClient` 및 `MetadataApiClient`와 동일한 클라이언트 패밀리에 속하지만, GraphQL API 대신 앱의 HTTP 라우트를 대상으로 합니다.
| 방법 | 설명 |
| --------------------------------- | ------------------------------ |
| `get(path, options?)` | `GET` 요청을 전송합니다. |
| `post(path, body?, options?)` | `POST` 요청을 전송합니다. |
| `put(path, body?, options?)` | `PUT` 요청을 전송합니다. |
| `patch(path, body?, options?)` | `PATCH` 요청을 전송합니다. |
| `delete(path, options?)` | `DELETE` 요청을 전송합니다. |
| `request(method, path, options?)` | 임의의 HTTP 메서드를 사용하는 일반적인 요청입니다. |
`options`는 `headers`, `query`(쿼리 문자열 파라미터의 레코드이며, nullish 값은 건너뜁니다), 그리고 `signal`을 통한 `AbortSignal`을 받습니다. `FormData`가 아닌 객체 `body`는 자동으로 JSON 직렬화됩니다. `401`이 발생하면 클라이언트는 호스트를 통해 한 번 액세스 토큰을 갱신한 뒤 요청을 재시도합니다.
기본 URL과 토큰은 기본적으로 환경에서 자동으로 결정됩니다. 테스트 등에서 필요할 때는 생성자에 override를 전달하세요. 예를 들면 다음과 같습니다:
```ts
const client = new RestApiClient({
baseUrl: 'https://api.example.com',
token: 'my-token',
});
```
실패한 요청은 `status`, `statusText`, `url`, 파싱된 `body`를 노출하는 `RestApiClientError`를 throw합니다:
```tsx
import { RestApiClient, RestApiClientError } from 'twenty-client-sdk/rest';
const client = new RestApiClient();
try {
const prs = await client.get('/s/github/fetch-prs', {
query: { state: 'open' },
});
} catch (error) {
if (error instanceof RestApiClientError) {
console.error(error.status, error.body);
}
}
```
## 런타임 컨텍스트에 접근하기
컴포넌트 내부에서, 현재 사용자, 레코드, 컴포넌트 인스턴스에 접근하려면 SDK 훅을 사용하세요:
```tsx src/front-components/record-info.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
useUserId,
useRecordId,
useFrontComponentId,
} from 'twenty-sdk/front-component';
const RecordInfo = () => {
const userId = useUserId();
const recordId = useRecordId();
const componentId = useFrontComponentId();
return (
<div>
<p>User: {userId}</p>
<p>Record: {recordId ?? 'No record context'}</p>
<p>Component: {componentId}</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f23456789012',
name: 'record-info',
component: RecordInfo,
});
```
사용 가능한 훅:
| 훅 | 반환값 | 설명 |
| --------------------------------------------- | --------------------- | ---------------------------------------------- |
| `useUserId()` | `string` 또는 `null` | 현재 사용자의 ID |
| `useSelectedRecordIds()` | `string[]` | 선택된 모든 기록 ID(선택된 기록이 없으면 빈 배열) |
| `useRecordId()` | `string` 또는 `null` | **사용 중단됨.** 대신 `useSelectedRecordIds()`를 사용하세요 |
| `useFrontComponentId()` | `string` | 이 컴포넌트 인스턴스의 ID |
| `useColorScheme()` | `'light'` 또는 `'dark'` | 호스트 UI의 활성 색 구성표 (`System`은 이미 결정됨) |
| `useFrontComponentExecutionContext(selector)` | 항목에 따라 다름 | 셀렉터 함수를 사용해 전체 실행 컨텍스트에 접근 |
## 애플리케이션 변수
[`defineApplication()`](/l/ko/developers/extend/apps/config/application)에서 `isSecret: false`로 정의된 애플리케이션 변수는 `getApplicationVariable` 유틸리티를 통해 프론트 컴포넌트 안에서 사용할 수 있습니다:
```tsx src/front-components/greeting.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const Greeting = () => {
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
return <p>Hello, {recipientName}!</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'greeting',
component: Greeting,
});
```
<Warning>
비밀 변수(`isSecret: true`)는 프론트 컴포넌트에 **노출되지 않습니다**. 이 변수들은 서버 측에서 실행되는 [로직 함수](/l/ko/developers/extend/apps/logic/logic-functions)에서만 사용할 수 있습니다. 이는 API 키와 같은 민감한 값이 브라우저로 전송되는 것을 방지합니다.
</Warning>
다음 시스템 변수는 항상 `process.env`를 통해 사용할 수 있습니다:
| 변수 | 설명 |
| ------------------------- | ------------------------ |
| `TWENTY_API_URL` | Twenty API의 기본 URL |
| `TWENTY_APP_ACCESS_TOKEN` | 앱의 역할 범위로 제한된 단기간 유효한 토큰 |
## 호스트 통신 API
프런트 컴포넌트는 `twenty-sdk`의 함수를 사용해 내비게이션, 모달, 알림을 트리거할 수 있습니다:
| 함수 | 설명 |
| ----------------------------------------------- | ------------- |
| `navigate(to, params?, queryParams?, options?)` | 앱 내 페이지로 이동 |
| `openSidePanelPage(params)` | 사이드 패널 열기 |
| `closeSidePanel()` | 사이드 패널을 닫습니다 |
| `openCommandConfirmationModal(params)` | 확인 대화상자 표시 |
| `enqueueSnackbar(params)` | 토스트 알림 표시 |
| `unmountFrontComponent()` | 컴포넌트를 언마운트합니다 |
| `updateProgress(progress)` | 진행률 표시기 업데이트 |
호스트 API를 사용하여 동작이 완료된 후 스낵바를 표시하고 사이드 패널을 닫는 예시는 다음과 같습니다:
```tsx src/front-components/archive-record.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { useRecordId } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### 여러 기록과의 작업
여러 개의 선택된 기록을 처리하려면 `useSelectedRecordIds()`를 사용하세요. 이는 일괄 작업에 유용합니다:
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
const BulkExport = () => {
const selectedRecordIds = useSelectedRecordIds();
const handleExport = async () => {
const client = new CoreApiClient();
for (const recordId of selectedRecordIds) {
await client.mutation({
updateTask: {
__args: { id: recordId, data: { exported: true } },
id: true,
},
});
}
await enqueueSnackbar({
message: `Exported ${selectedRecordIds.length} records`,
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Export {selectedRecordIds.length} selected record(s)?</p>
<button onClick={handleExport}>Export</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
name: 'bulk-export',
description: 'Export selected records',
component: BulkExport,
command: {
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
label: 'Bulk Export',
availabilityType: 'RECORD_SELECTION',
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
},
});
```
## 퍼블릭 에셋
프런트 컴포넌트는 `getPublicAssetUrl`을 사용해 앱의 `public/` 디렉터리의 파일에 접근할 수 있습니다:
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const Logo = () => <img src={getPublicAssetUrl('logo.png')} alt="Logo" />;
export default defineFrontComponent({
universalIdentifier: '...',
name: 'logo',
component: Logo,
});
```
자세한 내용은 [퍼블릭 에셋 섹션](/l/ko/developers/extend/apps/config/public-assets)을 참조하세요.
## 스타일링
프런트 컴포넌트는 여러 스타일링 방식을 지원합니다. 다음과 같은 방식을 사용할 수 있습니다:
* **인라인 스타일** — `style={{ color: 'red' }}`
* **Twenty UI 컴포넌트** — `twenty-sdk/ui`에서 임포트(버튼, 태그, 상태, 칩, 아바타 등)
* **Emotion** — `@emotion/react`를 사용하는 CSS-in-JS
* **Styled-components** — `styled.div` 패턴
* **Tailwind CSS** — 유틸리티 클래스
* React와 호환되는 **모든 CSS-in-JS 라이브러리**
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { Button, Tag, Status } from 'twenty-sdk/ui';
const StyledWidget = () => {
return (
<div style={{ padding: '16px', display: 'flex', gap: '8px' }}>
<Button title="Click me" onClick={() => alert('Clicked!')} />
<Tag text="Active" color="green" />
<Status color="green" text="Online" />
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-567890123456',
name: 'styled-widget',
component: StyledWidget,
});
```
@@ -0,0 +1,44 @@
---
title: 내비게이션 메뉴 항목
description: 워크스페이스 사이드바에 사용자 지정 항목을 추가하여 저장된 보기나 외부 URL에 연결하세요.
icon: bars
---
**내비게이션 메뉴 항목**은 왼쪽 사이드바의 항목입니다. `defineNavigationMenuItem()`을 사용해 사용자 지정 사이드바 링크를 추가하세요 — 일반적으로 배포하는 각 [보기](/l/ko/developers/extend/apps/layout/views)마다 하나씩 추가하거나 외부 URL을 가리키도록 설정합니다.
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
type: NavigationMenuItemType.VIEW,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
```
## 핵심 요점
* `type`은 메뉴 항목이 어디로 연결되는지를 결정합니다. 각 유형은 특정 식별자 필드와 짝을 이룹니다:
| 유형 | 하는 일 | 필수 필드 |
| ------------------------------------ | --------------------------- | -------------------------------------------------------------- |
| `NavigationMenuItemType.VIEW` | 저장된 보기를 엽니다 | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | 외부 URL을 엽니다 | `link` |
| `NavigationMenuItemType.FOLDER` | 중첩된 항목들을 하나의 레이블 아래로 그룹화합니다 | `name` (그리고 하위 항목들은 `folderUniversalIdentifier`를 통해 폴더를 참조합니다) |
| `NavigationMenuItemType.OBJECT` | 오브젝트의 기본 인덱스 페이지를 엽니다 | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | 독립형 페이지 레이아웃을 엽니다 | `pageLayoutUniversalIdentifier` |
* `position`은 사이드바에서의 정렬 순서를 제어합니다.
* `icon`과 `color`는 선택 사항이며 항목의 표시 방식을 사용자 지정합니다.
* `folderUniversalIdentifier`는 모든 항목에서 사용할 수 있으며, 이를 통해 해당 항목을 `FOLDER` 유형의 상위 항목 안에 중첩할 수 있습니다.
<Note>
**흔한 오류:** 연결된 보기와 내비게이션 메뉴 항목 없이 오브젝트를 생성하면 해당 오브젝트는 사용자에게 보이지 않습니다. 기술적/내부용 오브젝트가 아닌 한, 모든 사용자 지정 오브젝트에는 기본 보기와 이를 가리키는 사이드바 항목이 있어야 합니다.
</Note>
@@ -0,0 +1,56 @@
---
title: 개요
description: Twenty의 UI에 앱을 배치하세요 — 사이드바 항목, 저장된 보기, 레코드 페이지 탭, 샌드박스된 React 컴포넌트.
icon: table-columns
---
Twenty 앱의 **레이아웃 레이어**는 사용자가 보는 모든 것을 의미합니다. 앱이 사이드바 어디에 노출되는지, 어떤 목록 보기를 제공하는지, 레코드 상세 페이지가 어떻게 구성되는지, 그리고 어떤 커스텀 React 컴포넌트가 해당 페이지 내부에 렌더링되는지가 여기에 포함됩니다.
```text
Sidebar Record list Record detail page
─────── ─────────── ──────────────────
[📋 My View] ────▶ ┌──────────┐ ┌─────────────────────┐
[📋 Drafts ] │ Companies│ │ Tabs: [Overview ] │
[📋 Inbox ] │ ──────── │ │ [Notes ] │
▲ │ Apple │ │ [Hello ]◀──── definePageLayoutTab
│ │ Acme │ │ │ adds a tab...
└ defineNavi- │ … │ │ ┌────────────────┐ │
gationMenu- └────▲─────┘ │ │ │ │
Item points │ │ │ React UI │◀── …with a
to a defineView │ │ │ (sandboxed in │ │ defineFrontComponent
└ defineView │ │ a Worker) │ │ widget inside
picks columns │ └────────────────┘ │
and filters └─────────────────────┘
```
## 이 섹션에서 다루는 내용
<CardGroup cols={2}>
<Card title="보기" icon="list" href="/l/ko/developers/extend/apps/layout/views">
`defineView` — 표시 열, 필터, 그룹을 포함한 저장된 목록 구성입니다.
</Card>
<Card title="내비게이션 메뉴 항목" icon="bars" href="/l/ko/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — 보기 또는 외부 URL을 가리키는 사이드바 항목입니다.
</Card>
<Card title="페이지 레이아웃" icon="table-columns" href="/l/ko/developers/extend/apps/layout/page-layouts">
`definePageLayout` 및 `definePageLayoutTab` — 레코드 상세 페이지의 탭과 위젯을 정의합니다.
</Card>
<Card title="프런트 컴포넌트" icon="window-maximize" href="/l/ko/developers/extend/apps/layout/front-components">
`defineFrontComponent` — Twenty 내부에서 렌더링되는 샌드박스된 React 컴포넌트입니다.
</Card>
<Card title="명령 메뉴 항목" icon="터미널" href="/l/ko/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — 프런트 컴포넌트를 Cmd+K 항목과 빠른 작업으로 등록합니다.
</Card>
</CardGroup>
## 앱이 노출되는 위치
| 표시 위치 | 제어하는 항목 | 엔터티 |
| ----------------- | --------------------------------------- | ----------------------------------------- |
| **사이드바** | 저장된 보기 또는 외부 URL에 연결되는 커스텀 항목 | `defineNavigationMenuItem` |
| **레코드 목록** | 오브젝트에 대한 저장된 구성 — 표시되는 열, 정렬 순서, 필터, 그룹 | `defineView` |
| **레코드 상세 페이지** | 레코드 페이지(사용자 정의 오브젝트 또는 표준 오브젝트)의 탭과 위젯 | `definePageLayout`, `definePageLayoutTab` |
| **위의 어느 위치 내부** | 커스텀 React 위젯 — 버튼, 폼, 대시보드, 통합 기능 | `defineFrontComponent` |
| **명령 메뉴 (Cmd+K)** | 고정된 빠른 작업 또는 숨겨진 명령 | `defineCommandMenuItem` |
프런트 컴포넌트는 Remote DOM을 사용하는 격리된 Web Worker 내부에서 실행됩니다. 이들은 페이지 안에서 네이티브하게 렌더링되지만(iframe 내부가 아님), 호스트 페이지나 DOM에 직접 접근할 수는 없습니다. Twenty와의 통신은 메시지 전달 호스트 API를 통해 이루어집니다.
@@ -0,0 +1,132 @@
---
title: 페이지 레이아웃
description: "`definePageLayout` 및 `definePageLayoutTab`을 사용하여 레코드 상세 페이지의 탭, 위젯, 그리고 프런트 컴포넌트가 렌더링되는 위치를 사용자 지정합니다."
icon: table-columns
---
**페이지 레이아웃**은 레코드의 상세 페이지가 어떻게 구성되는지를 제어합니다. 어떤 탭이 표시되고, 그 탭 안에 어떤 위젯이 포함되는지를 결정합니다. 소유한 객체에 대한 레이아웃을 선언하려면 `definePageLayout()`을 사용하고, 이미 존재하는 레이아웃(사용자 소유 또는 표준 Twenty 레이아웃)에 단일 탭을 추가하려면 `definePageLayoutTab()`을 사용합니다.
| 사용 사례 | 엔터티 |
| ---------------------------------------------------- | --------------------- |
| 소유한 객체의 레코드 페이지에 대한 전체 레이아웃을 정의합니다. | `definePageLayout` |
| 기존 레이아웃(자신이 소유한 객체의 레이아웃이든, 표준 레이아웃이든)에 탭 하나를 추가합니다. | `definePageLayoutTab` |
## definePageLayout
전체 상세 페이지를 소유할 때 사용합니다. 일반적으로 직접 정의한 사용자 지정 객체에 해당합니다.
```ts src/page-layouts/example-record-page-layout.ts
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayout({
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
name: 'Example Record Page',
type: 'RECORD_PAGE',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
tabs: [
{
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
title: 'Hello World',
position: 50,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
```
### 핵심 요점
* `type`은 일반적으로 특정 객체의 상세 보기를 사용자 지정하기 위해 'RECORD_PAGE'입니다.
* `objectUniversalIdentifier`는 이 레이아웃이 적용되는 객체를 지정합니다.
* 각 `tab`은 `title`, `position`, `layoutMode`(`CANVAS`는 자유 형식 레이아웃)를 통해 페이지의 섹션을 정의합니다.
* 탭 내부의 각 `widget`은 [front component](/l/ko/developers/extend/apps/layout/front-components), 관계 목록 또는 기타 기본 제공 위젯 타입을 렌더링할 수 있습니다.
* 탭의 `position`은 탭의 순서를 제어합니다. 값을 더 높게 설정하면(예: 50) 기본 탭 뒤에 사용자 지정 탭을 배치할 수 있습니다.
## definePageLayoutTab
기존 레이아웃에 탭만 추가하려는 경우에 사용합니다. 예를 들어, 표준 Company 페이지에 분석 탭을 추가하거나, 자신이 소유한 객체의 레이아웃에 AI 요약 탭을 연결하는 경우입니다.
```ts src/page-layouts/example-extra-tab.ts
import {
definePageLayoutTab,
PageLayoutTabLayoutMode,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from '../front-components/hello-world';
export default definePageLayoutTab({
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
pageLayoutUniversalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage
.universalIdentifier,
title: 'Hello World',
position: 1000,
icon: 'IconWorld',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000002',
title: 'Hello World',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
});
```
### 핵심 요점
* `pageLayoutUniversalIdentifier`는 **필수**이며, 설치 시점에 이미 존재하는 페이지 레이아웃을 가리켜야 합니다. 이는 표준 Twenty 레이아웃이거나, 사용자의 앱에서 정의한 레이아웃일 수 있습니다. 현재는 설치된 다른 앱이 소유한 레이아웃에 대한 크로스 앱 참조는 지원되지 않습니다. 상위 레이아웃이 없으면, 명확한 유효성 검사 오류와 함께 설치가 실패합니다.
* 표준 Twenty 레이아웃의 경우 `twenty-sdk/define`에서 식별자를 가져옵니다:
```ts
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.companyRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.opportunityRecordPage.universalIdentifier
// STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.noteRecordPage.universalIdentifier
// …
```
각 레이아웃 항목은 `tabs`와 해당 `widgets`도 노출하므로, 모든 수준을 참조할 수 있습니다:
```ts
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.universalIdentifier
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.taskRecordPage.tabs.home.widgets.fields.universalIdentifier
```
짧은 별칭인 `STANDARD_PAGE_LAYOUT`도 사용할 수 있습니다:
```ts
import { STANDARD_PAGE_LAYOUT } from 'twenty-sdk/define';
STANDARD_PAGE_LAYOUT.companyRecordPage.universalIdentifier;
```
* `widgets`는 이 탭에만 범위가 국한됩니다. `definePageLayout`에 인라인으로 정의된 위젯과 완전히 동일한 방식으로 [front components](/l/ko/developers/extend/apps/layout/front-components), 뷰 등을 참조합니다.
* `position`은 대상 레이아웃의 기존 탭과의 순서를 제어합니다. 기본 제공 탭을 기준으로 원하는 위치에 탭이 놓이도록 값을 선택하세요.
* 기존 레이아웃에만 추가하려는 경우에는 `definePageLayout` 대신 이것을 사용하세요. 전체 레이아웃을 소유하는 경우에는 `definePageLayout`을 사용하세요.
@@ -0,0 +1,97 @@
---
title: 보기
description: 앱의 객체에 대해 열 순서, 필터, 그룹이 미리 구성된 저장된 보기를 제공하세요.
icon: list
---
**보기**는 객체 레코드가 어떻게 표시될지에 대한 저장된 구성입니다. 어떤 필드가 표시되는지, 그 순서, 표시 여부, 적용된 필터나 그룹 등을 정의합니다. `defineView()`를 사용해 앱에 미리 구성된 보기를 포함할 수 있습니다. 일반적으로 생성하는 각 커스텀 객체마다 기본 목록 보기를 제공합니다.
```ts src/views/example-view.ts
import { defineView, ViewKey } from 'twenty-sdk/define';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
import { NAME_FIELD_UNIVERSAL_IDENTIFIER } from '../objects/example-object';
export default defineView({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
key: ViewKey.INDEX,
position: 0,
fields: [
{
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
```
## 핵심 요점
* `objectUniversalIdentifier`는 이 뷰가 적용되는 객체를 지정합니다. 이 객체는 사용자가 정의한 커스텀 객체일 수도 있고, 표준 Twenty 객체일 수도 있습니다.
* `key`는 보기 유형을 결정합니다. `ViewKey.INDEX`는 해당 객체의 기본 목록 보기입니다.
* `fields`는 어떤 열을 어떤 순서로 표시할지를 제어합니다. 각 필드는 `fieldMetadataUniversalIdentifier`를 참조합니다.
* 또한 더 고급 구성을 위해 `filters`, `filterGroups`, `groups`, `fieldGroups`를 선언할 수 있습니다.
* `position`은 동일한 객체에 여러 뷰가 있을 때의 정렬 순서를 제어합니다.
## 필터
뷰는 미리 적용된 필터와 함께 제공될 수 있습니다. 각 필터에는 세 가지 요소가 있습니다: 필터링할 **필드**, **연산자**(어떻게 비교할지), 그리고 **값**(무엇과 비교할지). 세 가지가 모두 맞아야 하며, 필드 유형에 적용되지 않는 연산자를 사용하면 동기화 시 거부됩니다.
```ts
import { ViewFilterOperand } from 'twenty-shared/types';
filters: [
{
universalIdentifier: '...',
fieldMetadataUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
operand: ViewFilterOperand.IS,
value: ['ACTIVE'],
},
],
```
### 필드 유형별 지원되는 연산자
| 필드 유형 | 지원되는 피연산자 |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `TEXT`, `EMAILS`, `FULL_NAME`, `ADDRESS`, `LINKS`, `PHONES`, `RAW_JSON`, `FILES`, `ACTOR`, `ARRAY` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `ACTOR.source`, `ACTOR.workspaceMemberId` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `SELECT` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `MULTI_SELECT` | `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RELATION` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `NUMBER` | `IS`, `IS_NOT`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `RATING` | `IS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY`, `CURRENCY.amountMicros` | `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `CURRENCY.currencyCode` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `DATE`, `DATE_TIME` | `IS`, `IS_RELATIVE`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `BOOLEAN` | `IS` |
| `UUID` | `IS`, `IS_NOT`, `IS_EMPTY`, `IS_NOT_EMPTY` |
| `TS_VECTOR` | `VECTOR_SEARCH` |
> 유사한 이름을 가진 필드 타입이라도 전혀 다른 피연산자를 사용할 수 있습니다. 대표적인 예로 `SELECT`와 `MULTI_SELECT`가 있습니다.
### 피연산자별 값 구조
`value` 필드는 항상 JSON으로 직렬화할 수 있는 값이지만, 기대되는 구조는 피연산자에 따라 달라집니다:
| 피연산자 그룹 | 값 구조 | 예시 |
| ----------------------------------------------------- | --------------------- | ------------------------ |
| `SELECT`에서의 `IS`, `IS_NOT` | 옵션 키(문자열) 배열 | `['ACTIVE', 'PENDING']` |
| `MULTI_SELECT`에서의 `CONTAINS`, `DOES_NOT_CONTAIN` | 옵션 키(문자열) 배열 | `['TAG_A']` |
| `RELATION`에서의 `IS`, `IS_NOT` | 레코드 ID(uuid) 배열 | `['c5a1...']` |
| 텍스트 계열 필드에서의 `CONTAINS`, `DOES_NOT_CONTAIN` | 문자열 | `'acme'` |
| `NUMBER`에서의 `IS`, `IS_NOT` | 문자열(값) | `'5'` |
| `RATING` / `UUID`에서의 `IS` | 문자열(값) | `'5'` |
| `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL` | 문자열(경계값) | `'10'` |
| `DATE` / `DATE_TIME`에서의 `IS`, `IS_BEFORE`, `IS_AFTER` | ISO 8601 문자열 | `'2025-01-01T00:00:00Z'` |
| `IS_EMPTY`, `IS_NOT_EMPTY` | 빈 문자열 | `''` |
| `BOOLEAN`에서의 `IS` | `'true'` 또는 `'false'` | `'true'` |
## 보기가 UI에 표시되는 방식
보기만으로는 사이드바에서 직접 접근할 수 없습니다. 사이드바에 표시하려면, 해당 보기의 `universalIdentifier`를 가리키는 `VIEW` 타입의 [탐색 메뉴 항목](/l/ko/developers/extend/apps/layout/navigation-menu-items)과 연결해야 합니다. 이것이 표준적인 패턴입니다. 일반적으로 각 커스텀 객체는 기본 보기와, 그 보기를 여는 사이드바 항목을 함께 제공합니다.
@@ -0,0 +1,192 @@
---
title: 연결
description: 앱이 OAuth를 통해 타사 서비스에서 사용자를 대신해 동작하도록 하세요.
icon: plug
---
연결은 사용자가 외부 서비스(Linear, GitHub, Slack 등)에 대한 자격 증명을 보유하는 것을 말합니다. 앱은 해당 자격 증명을 **어떻게** 얻는지(즉, **연결 제공자**)를 선언하고, 런타임에 이를 사용해 서드파티 API에 인증된 호출을 수행합니다.
현재는 OAuth 2.0만 지원됩니다. 향후 자격 증명 유형(개인 액세스 토큰, API 키, 기본 인증)은 동일한 인터페이스에 연동되며 — 이미 `defineConnectionProvider({ type: 'oauth', ... })`를 사용하는 앱은 마이그레이션이 필요하지 않습니다.
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="앱의 연결을 얻는 방법을 선언합니다">
연결 제공자는 앱에 필요한 OAuth 핸드셰이크를 설명합니다. 사용자가 앱 설정에서 "연결 추가"를 클릭하고 제공자의 동의 화면을 완료하면, 워크스페이스에 `ConnectedAccount` 행이 생성됩니다.
정상 동작하려면 **두 개의 파일**이 필요합니다 — 연결 제공자, 그리고 OAuth 클라이언트 자격 증명을 보유하는 `defineApplication`의 해당 `serverVariables` 선언.
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
핵심 요점:
* `name`은 `listConnections({ providerName })`에서 사용하는 고유 식별자 문자열입니다(kebab-case, `^[a-z][a-z0-9-]*$`와 일치해야 함).
* `displayName`은 앱별 설정 탭과 AI 도구 목록에 표시됩니다.
* `clientIdVariable` / `clientSecretVariable`은 값이 아닌 **이름**이며, `defineApplication.serverVariables`에 선언된 키와 일치해야 합니다. 실제 `client_id`와 `client_secret`은 서버 관리자가 앱 등록 UI를 통해 입력하며, 저장소에 커밋되지 않습니다.
* `serverVariables`(`applicationVariables` 아님)을 사용하세요 — OAuth 자격 증명은 서버 전체에 적용되며 Twenty 서버마다 하나의 OAuth 앱만 사용합니다.
* 두 `serverVariables`가 모두 채워질 때까지, 앱별 설정 탭에는 "서버 관리자 필요" 힌트가 표시되고 "연결 추가" 버튼이 비활성화됩니다.
* `type: 'oauth'`는 현재 지원되는 유일한 값입니다. 구분자는 전방 호환됩니다: 향후 유형(`'pat'`, `'api-key'`, ...) `oauth`와 함께 새로운 하위 구성 블록이 추가됩니다.
제공자가 허용 목록에 추가해야 할 OAuth 콜백 URL은 다음과 같습니다:
```
https://<your-twenty-server>/auth/apps/callback
```
</Accordion>
<Accordion title="listConnections / getConnection" description="로직 함수에서 연결 사용">
로직 함수 핸들러 내부에서 `listConnections({ providerName })`는 지정된 제공자에 대한 이 앱의 `ConnectedAccount` 행을 갱신된 액세스 토큰과 함께 반환합니다.
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
각 연결에는 다음이 포함됩니다:
| 필드 | 설명 |
| ----------------- | --------------------------------------------------------------- |
| `id` | 고유한 행 ID; 단일 항목을 다시 가져오려면 `getConnection(id)`에 전달하세요. |
| `visibility` | `'user'`(한 워크스페이스 구성원에게만 비공개) 또는 `'workspace'`(모든 구성원과 공유) |
| `scopes` | 상위 제공자가 부여한 OAuth 권한(`visibility`와는 구별됨 — 서로 관련 없음) |
| `userWorkspaceId` | 소유자의 userWorkspace ID — HTTP 경로 트리거에서 "요청 사용자의 연결"을 선택할 때 유용합니다 |
| `accessToken` | 최신 OAuth 액세스 토큰(만료 시 자동으로 갱신됨) |
| `name` / `handle` | 연결의 표시 이름(OAuth 콜백 시 자동으로 결정되며, 사용자가 이름을 변경할 수 있음) |
| `authFailedAt` | 가장 최근 갱신이 실패했을 때 설정됩니다; 사용자가 다시 연결해야 합니다 |
핵심 요점:
* 제공자별로 필터링하려면 `{ providerName }`를 전달하세요; 생략하면 이 앱이 모든 제공자에서 보유한 모든 연결을 가져옵니다.
* 서버는 반환하기 전에 액세스 토큰을 투명하게 갱신합니다. 핸들러는 항상 사용 가능한 토큰(또는 `authFailedAt`이 설정된 상태)을 보게 됩니다.
* `getConnection(id)`는 단일 행 버전입니다.
</Accordion>
<Accordion title="사용자별 대 워크스페이스 공유 가시성" description="사용자가 비공개 자격 증명과 공유 자격 증명 사이를 선택하는 방법">
사용자가 "연결 추가"를 클릭하면, 가시성을 선택하라는 프롬프트가 표시됩니다:
* **나만 사용** — 해당 자격 증명은 연결한 사용자에게만 비공개입니다. 그 사용자를 대신해 호출되는 모든 로직 함수(`isAuthRequired: true`가 설정된 HTTP 경로 트리거)는 이를 볼 수 있습니다; cron 트리거와 데이터베이스 이벤트는 볼 수 없습니다.
* **워크스페이스 공유** — 모든 워크스페이스 구성원이 해당 자격 증명을 사용할 수 있습니다. 요청 사용자가 없으므로 Cron/데이터베이스 트리거도 이를 볼 수 있습니다.
각 핸들러에 맞는 유형을 사용하세요:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
각 (사용자, 제공자)당 여러 연결이 허용되므로, 동일한 사용자가 "Personal Linear"와 "Work Linear"를 나란히 보유할 수 있습니다.
</Accordion>
<Accordion title="일회성 제공자 설정" description="서드파티 서비스에 OAuth 앱을 등록하세요">
각 연결 제공자마다 서버 관리자가 먼저 서드파티에 OAuth 앱을 등록해야 합니다.
1. 제공자의 개발자 설정으로 이동하세요(예: https://linear.app/settings/api/applications/new).
2. **Redirect URI**를 `\<SERVER_URL>/auth/apps/callback`으로 설정하세요.
3. 생성된 **Client ID**와 **Client Secret**을 복사하세요.
4. 서버 관리자 권한으로 Twenty에서 설치된 앱을 열고 → 해당 `serverVariables`에 값을 설정하세요.
5. 그런 다음 워크스페이스 구성원은 앱별 **연결** 섹션에서 연결을 추가할 수 있습니다.
</Accordion>
</AccordionGroup>
@@ -0,0 +1,514 @@
---
title: 로직 함수
description: HTTP, cron 및 데이터베이스 이벤트 트리거를 사용하여 서버 측 TypeScript 함수를 정의합니다.
icon: bolt
---
로직 함수는 Twenty 플랫폼에서 실행되는 서버 측 TypeScript 함수입니다. 이 함수들은 HTTP 요청, cron 스케줄 또는 데이터베이스 이벤트에 의해 트리거될 수 있으며 — AI 에이전트를 위한 도구로도 제공할 수 있습니다.
<AccordionGroup>
<Accordion title="defineLogicFunction" description="로직 함수와 해당 트리거 정의">
각 함수 파일은 `defineLogicFunction()`을 사용해 핸들러와 선택적 트리거가 포함된 구성을 내보냅니다.
```ts src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import type { RoutePayload } from 'twenty-sdk/logic-function';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const body = (params.body ?? {}) as { name?: string };
const name = body.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
httpRouteTriggerSettings: {
path: '/post-card/create',
httpMethod: 'POST',
isAuthRequired: true,
},
/*databaseEventTriggerSettings: {
eventName: 'people.created',
},*/
/*cronTriggerSettings: {
pattern: '0 0 1 1 *',
},*/
});
```
사용 가능한 트리거 유형:
* **httpRoute**: **`/s/` 엔드포인트** 아래의 HTTP 경로와 메서드로 함수를 노출합니다:
> 예: `path: '/post-card/create'`는 `https://your-twenty-server.com/s/post-card/create`에서 호출할 수 있습니다
<Note>
(헤드리스) 프런트 컴포넌트에서 라우트로 트리거되는 로직 함수를 호출하려면 [로직 함수 호출하기](/l/ko/developers/extend/apps/layout/front-components#calling-a-logic-function)를 참고하세요.
</Note>
* **cron**: CRON 식을 사용하여 예약된 일정으로 함수를 실행합니다.
* **databaseEvent**: 워크스페이스 객체 라이프사이클 이벤트에서 실행됩니다. 이벤트 작업이 `updated`인 경우, 수신할 특정 필드를 `updatedFields` 배열에 지정할 수 있습니다. 정의하지 않거나 비워두면, 어떤 업데이트든 함수가 트리거됩니다.
> 예: `person.updated`, `*.created`, `company.*`
<Note>
CLI를 사용해 함수를 수동으로 실행할 수도 있습니다:
```bash filename="Terminal"
yarn twenty dev:function:exec -n create-new-post-card -p '{"key": "value"}'
```
```bash filename="Terminal"
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
다음으로 로그를 확인할 수 있습니다:
```bash filename="Terminal"
yarn twenty dev:function:logs
```
</Note>
#### 라우트 트리거 페이로드
라우트 트리거가 로직 함수를 호출하면, 함수는 [AWS HTTP API v2 형식](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html)을 따르는 `RoutePayload` 객체를 받습니다.
`twenty-sdk/logic-function`에서 `RoutePayload` 타입을 임포트하세요:
```ts
import type { RoutePayload } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { headers, queryStringParameters, pathParameters, body } = event;
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
`RoutePayload` 타입은 다음과 같은 구조입니다:
| 속성 | 유형 | 설명 | 예시 |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record\<string, string \| undefined>` | HTTP 헤더(`forwardedRequestHeaders`에 나열된 항목만) | 아래 섹션 참조 |
| `queryStringParameters` | `Record\<string, string \| undefined>` | 쿼리 문자열 매개변수(여러 값은 쉼표로 연결됨) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record\<string, string \| undefined>` | 라우트 패턴에서 추출된 경로 매개변수 | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | 파싱된 요청 본문(JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | JSON 파싱 이전의 원본 UTF-8 요청 본문입니다. HMAC 스타일 웹훅 서명을 검증하는 데 유용합니다(예: GitHub의 `X-Hub-Signature-256`, Stripe). 런타임이 이를 보존하지 않은 경우에는 `undefined`입니다. | |
| `isBase64Encoded` | `boolean` | 본문이 base64로 인코딩되었는지 여부 | |
| `requestContext.http.method` | `string` | HTTP 메서드(GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | 원시 요청 경로 | |
#### forwardedRequestHeaders
기본적으로 보안상의 이유로 들어오는 요청의 HTTP 헤더는 로직 함수로 **전달되지 않습니다**.
특정 헤더에 접근하려면 `forwardedRequestHeaders` 배열에 나열하세요:
```ts
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
httpRouteTriggerSettings: {
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
});
```
핸들러에서, 전달된 헤더에 다음과 같이 접근합니다:
```ts
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
헤더 이름은 소문자로 정규화됩니다. 소문자 키를 사용해 접근하세요(예: `event.headers['content-type']`).
</Note>
#### 사용자 정의 HTTP 응답
기본적으로, 핸들러에서 일반 값을 반환하면 해당 값이 `200` 응답으로 전송됩니다(객체는 JSON, 문자열은 `text/plain`). 상태 코드와 응답 헤더를 제어하려면 `twenty-sdk/logic-function`에서 `Response`를 반환하세요:
```ts
import { Response } from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
return new Response('<h1>Hello</h1>', {
status: 201,
headers: { 'content-type': 'text/html' },
});
};
```
보안상의 이유로, 응답 헤더는 허용 목록으로 제한됩니다. 목록에 없는 헤더(예: `Set-Cookie`, `Access-Control-Allow-Origin`과 같은 CORS 헤더, 또는 사용자 정의 `X-*` 헤더)는 응답이 전송되기 전에 조용히 제거됩니다. 허용되는 응답 헤더는 다음과 같습니다:
* `content-type`
* `content-language`
* `content-disposition`
* `cache-control`
* `retry-after`
<Note>
상태 코드는 유효한 HTTP 상태 코드(100에서 599 사이)여야 합니다. 응답 헤더 이름은 대소문자를 구분하지 않습니다.
</Note>
#### 데이터베이스 이벤트 트리거 페이로드
데이터베이스 이벤트 트리거가 로직 함수를 호출하면, 변경된 각 레코드마다 하나의 `DatabaseEventPayload`를 받습니다. 이 페이로드는 소스 워크스페이스와 오브젝트에 대한 메타데이터를 레코드 수준 이벤트와 결합합니다.
```ts
import type {
DatabaseEventPayload,
ObjectRecordCreateEvent,
ObjectRecordDestroyEvent,
ObjectRecordUpdateEvent,
} from 'twenty-sdk/logic-function';
type Person = {
id: string;
emails?: { primaryEmail?: string };
};
```
페이로드에는 다음이 포함됩니다:
| 속성 | 설명 |
| ------------------------------------------------ | ------------------------------------------------------------------------------ |
| `name` | `person.updated`와 같은 이벤트 이름입니다. |
| `workspaceId` | 이벤트가 발생한 워크스페이스입니다. |
| `objectMetadata` | 변경된 오브젝트에 대한 메타데이터입니다. |
| `recordId` | 변경된 레코드의 ID입니다. |
| `userId`, `userWorkspaceId`, `workspaceMemberId` | 워크스페이스 사용자가 이벤트를 발생시켰을 때의 액터 필드입니다. |
| `properties` | 이벤트에 대한 레코드 데이터로, 작업 유형에 따라 `before`, `after`, `diff`, `updatedFields`를 포함합니다. |
| 이벤트 | 레코드 데이터 |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `person.created` | `event.properties.after` |
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
| `person.destroyed` | `event.properties.before` |
소프트 삭제의 경우, 레코드의 `deletedAt` 필드가 변경되므로 `.deleted`는 업데이트 스타일 구조를 따릅니다.
영구 삭제의 경우 `.destroyed`를 사용하세요.
<Note>
`databaseEventTriggerSettings.updatedFields`는 어떤 업데이트 이벤트가 함수를 트리거할지 필터링합니다.
`event.properties.updatedFields`는 현재 이벤트에서 실제로 어떤 필드가 변경되었는지 알려줍니다.
</Note>
생성 이벤트 예시:
```ts
type PersonCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Person>
>;
const handler = async (event: PersonCreatedEvent) => {
const person = event.properties.after;
return {
personId: event.recordId,
email: person.emails?.primaryEmail,
};
};
```
업데이트 이벤트 예시:
```ts
type PersonUpdatedEvent = DatabaseEventPayload<
ObjectRecordUpdateEvent<Person>
>;
const handler = async (event: PersonUpdatedEvent) => {
const { before, after, diff, updatedFields } = event.properties;
return {
personId: event.recordId,
updatedFields,
previousEmail: before.emails?.primaryEmail,
currentEmail: after.emails?.primaryEmail,
emailDiff: diff.emails,
};
};
```
이메일 업데이트에만 트리거되도록:
```ts
export default defineLogicFunction({
...,
databaseEventTriggerSettings: {
eventName: 'person.updated',
updatedFields: ['emails'],
},
});
```
삭제 이벤트 예시:
```ts
type PersonDestroyedEvent = DatabaseEventPayload<
ObjectRecordDestroyEvent<Person>
>;
const handler = async (event: PersonDestroyedEvent) => {
const personBeforeDestroy = event.properties.before;
return {
personId: event.recordId,
email: personBeforeDestroy.emails?.primaryEmail,
};
};
```
#### 함수를 AI 도구 또는 워크플로 작업으로 노출하기
로직 함수는 두 가지 영역에서 노출될 수 있으며, 각 영역마다 자체 트리거가 있습니다:
* **`toolTriggerSettings`** — 이 설정을 사용하면 함수가 Twenty의 AI 기능(채팅, MCP, 함수 호출)에서 검색 가능해집니다. 표준 JSON 스키마(LLM이 기본적으로 이해하는 형식)를 사용합니다.
* **`workflowActionTriggerSettings`** — 시각적 워크플로 빌더에서 함수를 단계로 나타나게 합니다. 빌더가 적절한 필드 에디터, 변수 선택기, 레이블을 렌더링할 수 있도록 Twenty의 풍부한 `InputSchema`를 사용합니다.
함수는 둘 중 하나만 또는 둘 다 선택할 수 있습니다. 이들 설정은 `cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`와 나란히 위치하며 — 패턴도 형태도 동일합니다.
```ts src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
toolTriggerSettings: {},
});
```
핵심 요점:
* 함수는 노출 방식을 혼합할 수 있습니다 — `toolTriggerSettings`와 `workflowActionTriggerSettings`를 모두 선언하여 채팅과 워크플로 빌더 모두에 노출할 수 있습니다.
* `toolTriggerSettings.inputSchema`와 `workflowActionTriggerSettings.inputSchema`는 모두 선택 사항입니다. 생략되면 매니페스트 빌더가 핸들러 소스 코드에서 이를 추론합니다(AI 도구의 경우 JSON 스키마, 워크플로 작업의 경우 Twenty의 `InputSchema`). 더 풍부한 타입 지정을 원한다면 명시적으로 제공하세요 — 예를 들어 워크플로 빌더를 위해 `CURRENCY` 또는 `RELATION`처럼 `FieldMetadataType`을 인지하는 필드를 사용하거나, AI 에이전트가 읽을 수 있는 `description` 필드를 포함하려는 경우:
```ts
export default defineLogicFunction({
...,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
},
});
```
<Note>
**좋은 `description`을 작성하세요.** AI 에이전트는 도구를 언제 사용할지 결정하기 위해 함수의 `description` 필드에 의존합니다. 도구가 무엇을 하는지와 언제 호출해야 하는지 구체적으로 작성하세요.
</Note>
</Accordion>
</AccordionGroup>
<Note>
**설치 훅** — 사전 설치 및 사후 설치 핸들러 — 는 이 런타임을 공유하지만, 각각의 `define` 함수로 선언되며 트리거 설정을 받지 않습니다. `definePreInstallLogicFunction` 및 `definePostInstallLogicFunction` 에 대해서는 [설치 훅](/l/ko/developers/extend/apps/config/install-hooks)을 참고하세요.
</Note>
## 타입이 지정된 API 클라이언트(twenty-client-sdk)
`twenty-client-sdk` 패키지는 로직 함수와 프런트 컴포넌트에서 Twenty API와 상호작용하기 위한 타입이 지정된 두 가지 GraphQL 클라이언트를 제공합니다.
| 클라이언트 | 가져오기 | 엔드포인트 | 생성 여부? |
| ------------------- | ---------------------------- | -------------------------------- | -------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — 워크스페이스 데이터(레코드, 객체) | 예, 개발/빌드 시점 |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — 워크스페이스 구성, 파일 업로드 | 아니오, 사전 빌드로 제공 |
<AccordionGroup>
<Accordion title="CoreApiClient" description="워크스페이스 데이터(레코드, 객체) 조회 및 변경">
`CoreApiClient`는 워크스페이스 데이터를 조회하고 변경하는 데 사용하는 기본 클라이언트입니다. `yarn twenty dev` 또는 `yarn twenty dev:build` 중에 **워크스페이스 스키마로부터 생성되므로**, 객체와 필드에 정확히 맞는 완전한 타입 정보를 제공합니다.
```ts
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: {
primaryLinkLabel: true,
primaryLinkUrl: true,
},
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
이 클라이언트는 선택 집합 구문을 사용합니다: 필드를 포함하려면 `true`를 전달하고, 인수에는 `__args`를 사용하며, 관계는 객체를 중첩합니다. 워크스페이스 스키마를 기반으로 완전한 자동 완성과 타입 검사를 제공합니다.
<Note>
**CoreApiClient는 개발/빌드 시점에 생성됩니다.** 먼저 `yarn twenty dev` 또는 `yarn twenty dev:build`를 실행하지 않고 사용하면 오류가 발생합니다. 생성은 자동으로 이루어지며 — CLI가 워크스페이스의 GraphQL 스키마를 분석하고 `@genql/cli`를 사용해 타입이 지정된 클라이언트를 생성합니다.
</Note>
#### 타입 주석을 위한 CoreSchema 사용
`CoreSchema`는 워크스페이스 객체에 맞는 TypeScript 타입을 제공하며, 컴포넌트 상태나 함수 매개변수에 타입을 지정할 때 유용합니다:
```ts
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
</Accordion>
<Accordion title="MetadataApiClient" description="워크스페이스 구성, 애플리케이션, 파일 업로드">
`MetadataApiClient`는 SDK와 함께 사전 빌드된 상태로 제공됩니다(생성 필요 없음). 워크스페이스 구성, 애플리케이션 및 파일 업로드를 위해 `/metadata` 엔드포인트를 조회합니다.
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// List first 10 objects in the workspace
const { objects } = await metadataClient.query({
objects: {
edges: {
node: {
id: true,
nameSingular: true,
namePlural: true,
labelSingular: true,
isCustom: true,
},
},
__args: {
filter: {},
paging: { first: 10 },
},
},
});
```
#### 파일 업로드
`MetadataApiClient`에는 파일 유형 필드에 파일을 첨부하기 위한 `uploadFile` 메서드가 포함되어 있습니다:
```ts
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
| 매개변수 | 유형 | 설명 |
| ---------------------------------- | -------- | --------------------------------------------- |
| `fileBuffer` | `Buffer` | 원시 파일 내용 |
| `filename` | `string` | 파일의 이름(저장 및 표시용) |
| `contentType` | `string` | MIME 유형(생략하면 기본값은 `application/octet-stream`) |
| `fieldMetadataUniversalIdentifier` | `string` | 객체의 파일 유형 필드의 `universalIdentifier` |
핵심 요점:
* 필드의 `universalIdentifier`(워크스페이스별 ID가 아님)를 사용하므로, 앱이 설치된 모든 워크스페이스에서 업로드 코드가 동작합니다.
* 반환된 `url`은 업로드된 파일에 액세스하는 데 사용할 수 있는 서명된 URL입니다.
</Accordion>
</AccordionGroup>
<Note>
코드가 Twenty에서 실행될 때(로직 함수 또는 프런트 컴포넌트), 플랫폼이 자격 증명을 환경 변수로 주입합니다:
* `TWENTY_API_URL` — Twenty API의 기본 URL
* `TWENTY_APP_ACCESS_TOKEN` — 애플리케이션의 기본 함수 역할 범위로 제한된 단기 키
클라이언트에 이를 전달할 필요가 없습니다 — 자동으로 `process.env`에서 읽습니다. API 키의 권한은 `defineApplicationRole()`으로 선언된 역할(또는 `application-config.ts`의 `defaultRoleUniversalIdentifier`를 통해 참조된 역할)에 의해 결정됩니다.
</Note>
@@ -0,0 +1,55 @@
---
title: 개요
description: HTTP 경로, 크론 스케줄, 데이터베이스 이벤트, AI 도구 또는 워크플로우 액션에 의해 트리거되어 Twenty 내부에서 실행되는 서버 사이드 TypeScript입니다.
icon: bolt
---
Twenty 앱의 **로직 계층**은 *실행되는* 코드로, HTTP 요청, 크론 스케줄, 레코드 변경에 반응하는 서버 사이드 TypeScript 핸들러, 워크스페이스 내부에 존재하는 AI 스킬과 에이전트, 그리고 서드파티 서비스에서 사용자를 대신해 함수가 동작할 수 있도록 하는 OAuth 연결로 구성됩니다.
```text
┌─ HTTP route ──┐
│ Cron schedule │
│ Database event │ ┌────────────────────┐
triggers ─┤ AI tool call ├─────▶│ Logic function │
│ Workflow action │ │ (your handler) │
│ Manual exec │ └────────────────────┘
└────────────────────┘ │
┌────────────────────────────┐
│ Twenty API (records) │
│ Third-party API │
│ (via Connection token) │
└────────────────────────────┘
```
## 이 섹션에서 다루는 내용
<CardGroup cols={2}>
<Card title="로직 함수" icon="bolt" href="/l/ko/developers/extend/apps/logic/logic-functions">
핵심 빌딩 블록 — 트리거 유형, 페이로드, 그리고 타입이 지정된 API 클라이언트입니다.
</Card>
<Card title="스킬 & 에이전트" icon="robot" href="/l/ko/developers/extend/apps/logic/skills-and-agents">
재사용 가능한 AI 에이전트 지시문과 맞춤형 시스템 프롬프트를 사용하는 어시스턴트입니다.
</Card>
<Card title="연결" icon="plug" href="/l/ko/developers/extend/apps/logic/connections">
Linear, GitHub, Slack 등 서드파티 서비스를 위해 앱이 보유한 OAuth 자격 증명입니다.
</Card>
</CardGroup>
## 한눈에 보는 트리거 유형
로직 함수는 하나 이상의 트리거를 선택합니다. 아래의 각 항목은 `defineLogicFunction()`의 개별 필드입니다:
| 트리거 | 실행 시점 | 설정 |
| -------------- | ---------------------------------------------- | ------------------------------- |
| **HTTP 경로** | 요청이 `/s/\<path>` 엔드포인트에 도달할 때 | `httpRouteTriggerSettings` |
| **크론** | CRON 표현식이 일치할 때 | `cronTriggerSettings` |
| **데이터베이스 이벤트** | 워크스페이스 레코드가 생성, 업데이트 또는 삭제될 때 | `databaseEventTriggerSettings` |
| **AI 도구** | Twenty AI 기능이 사용자의 함수를 호출하기로 결정할 때 | `toolTriggerSettings` |
| **워크플로우 액션** | 워크플로우 단계가 사용자의 함수를 호출할 때 | `workflowActionTriggerSettings` |
함수는 격리된 Node.js 프로세스의 샌드박스 환경에서 실행되며, [`defineApplication()`](/l/ko/developers/extend/apps/config/application)에 선언된 역할 범위에 맞춰 지정된 타입의 API 클라이언트를 통해 워크스페이스에 접근합니다.
<Note>
**설치 시점 후크** — 설치 전후에 실행되는 코드 — 는 동일한 런타임을 공유하지만, 별도의 define 함수들을 사용하며 [Config → Install Hooks](/l/ko/developers/extend/apps/config/install-hooks) 아래에 위치합니다.
</Note>
@@ -0,0 +1,138 @@
---
title: 스킬 & 에이전트
description: 앱용 AI 스킬 및 에이전트 정의.
icon: robot
---
<Warning>
스킬과 에이전트는 현재 알파 단계입니다. 해당 기능은 작동하지만 아직 발전 중입니다.
</Warning>
앱은 워크스페이스 내에서 동작하는 AI 기능을 정의할 수 있습니다 — 재사용 가능한 스킬 지침과 사용자 지정 시스템 프롬프트를 갖춘 에이전트.
<AccordionGroup>
<Accordion title="defineSkill" description="AI 에이전트 스킬 정의">
스킬은 워크스페이스 내에서 AI 에이전트가 사용할 수 있는 재사용 가능한 지침과 기능을 정의합니다. `defineSkill()`을 사용해 내장 검증과 함께 스킬을 정의하세요:
```ts src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk/define';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
핵심 요점:
* `name`은 스킬의 고유 식별자 문자열입니다(케밥 케이스 권장).
* `label`은 UI에 표시되는 사람이 읽기 쉬운 표시 이름입니다.
* `content`에는 스킬 지침이 포함됩니다 — 이는 AI 에이전트가 사용하는 텍스트입니다.
* `icon` (선택 사항)은 UI에 표시되는 아이콘을 설정합니다.
* `description` (선택 사항)은 스킬의 목적에 대한 추가 컨텍스트를 제공합니다.
</Accordion>
<Accordion title="defineAgent" description="사용자 지정 프롬프트로 AI 에이전트를 정의하세요">
에이전트는 워크스페이스 내에서 동작하는 AI 비서입니다. `defineAgent()`를 사용하여 사용자 지정 시스템 프롬프트로 에이전트를 생성하세요:
```ts src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'Helps the sales team draft outreach emails and research prospects',
icon: 'IconRobot',
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
```
핵심 요점:
* `name`은 에이전트의 고유 식별자 문자열입니다(케밥 케이스 권장).
* `label`은 UI에 표시되는 표시 이름입니다.
* `prompt`는 에이전트의 동작을 정의하는 시스템 프롬프트입니다.
* `description` (선택 사항)은 에이전트가 수행하는 작업에 대한 컨텍스트를 제공합니다.
* `icon` (선택 사항)은 UI에 표시되는 아이콘을 설정합니다.
* `modelId` (선택 사항)은 에이전트가 사용하는 기본 AI 모델을 재정의합니다.
* `responseFormat`(선택 사항)은 에이전트 출력의 형태를 제어합니다. 자유 형식 텍스트의 기본값은 `{ type: 'text' }`입니다. 구조화된 JSON 출력을 강제하려면 `{ type: 'json', schema }`를 사용하세요.
기본적으로 에이전트는 자유 형식 텍스트를 반환합니다. 구조화된 출력을 받으려면, `responseFormat`을 `{ type: 'json' }`으로 설정하고 `schema`를 제공하세요:
```ts src/agents/structured-agent.ts
import { defineAgent } from 'twenty-sdk/define';
export default defineAgent({
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
name: 'lead-scorer',
label: 'Lead Scorer',
prompt: 'Score the lead and explain your reasoning.',
responseFormat: {
type: 'json',
schema: {
type: 'object',
properties: {
score: { type: 'number', description: 'Lead score from 0 to 100' },
summary: { type: 'string', description: 'Short reasoning for the score' },
},
required: ['score', 'summary'],
additionalProperties: false,
},
},
});
```
스키마 참고:
* 스키마는 단일 수준의 객체입니다. 각 속성의 `type`은 반드시 기본형(`string`, `number`, 또는 `boolean`)이어야 합니다. 중첩 객체와 배열은 지원되지 않습니다.
* 각 속성의 `description`(선택 사항)은 해당 위치에 무엇을 넣어야 하는지 모델을 안내합니다.
* `required` (선택 사항)은 모델이 항상 반환해야 하는 속성을 나열합니다.
* `additionalProperties: false` (선택 사항)은 `properties`에 선언되지 않은 모든 속성을 금지합니다.
</Accordion>
<Accordion title="runAgent" description="로직 함수에서 에이전트를 실행">
`runAgent()`를 사용하면 로직 함수가 앱의 에이전트(해당 skills 및 tools 포함) 중 하나를 실행할 수 있습니다. `defineAgent()`에 전달했던 `universalIdentifier`로 에이전트를 식별합니다:
```ts src/logic-functions/run-enricher.ts
import { runAgent } from 'twenty-sdk/logic-function';
const { result, error, success } = await runAgent({
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
});
```
핵심 요점:
* 에이전트는 **동기적으로** 실행되며, 자체 tools를 통해 레코드를 읽고/업데이트할 수 있습니다. 실행이 완료되면 `runAgent()`가 resolve됩니다.
* 앱은 자신의 에이전트만 실행할 수 있습니다.
* 앱의 [기본 역할](/l/ko/developers/extend/apps/config/roles)은 `AI` 권한 플래그를 부여해야 합니다. 이를 위해 `permissionFlagUniversalIdentifiers`에 `SystemPermissionFlag.AI`를 추가하거나, `canAccessAllTools: true`로 설정하세요.
이 권한이 없으면 `runAgent()`는 권한 오류로 실패합니다.
* 로직 함수에 충분히 큰 `timeoutSeconds`를 설정하세요. 에이전트 실행에는 여러 초가 걸릴 수 있습니다.
* 실행이 완료되면 `success`는 `true`이고 `result`는 null이 아닙니다. 실패 시 `success`는 `false`, `result`는 `null`이며, `error`에 이유가 들어 있습니다(예: 실행 도중 워크스페이스의 AI 크레딧이 소진된 경우).
```ts src/roles/default-role.ts
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
export default defineApplicationRole({
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'Default function role',
// runAgent() requires the AI permission flag on the app's default role.
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
});
```
<Warning>
**루프를 피하세요:** `*.updated` 데이터베이스 이벤트 트리거에서 `runAgent()`를 호출하고 에이전트가 동일한 레코드를 업데이트하는 경우, `updatedFields`를 에이전트가 절대 쓰지 않는 필드(예: 원본 URL)로 한정하거나, `runAgent()`를 호출하기 전에 대상 필드 중 어느 것이든 아직 비어 있는지 여부를 검사해 가드하세요.
</Warning>
</Accordion>
</AccordionGroup>
@@ -0,0 +1,105 @@
---
title: CLI
description: "`yarn twenty`는 함수를 실행하고, 로그를 스트리밍하며, 앱 설치를 관리하고, 리모트를 전환하기 위한 명령들을 제공합니다."
icon: 터미널
---
`dev`, `dev:build`, `dev:add`, `dev:typecheck` 외에도, `yarn twenty` CLI는 함수 실행, 로그 보기, 앱 설치 관리용 명령을 제공합니다.
## 함수 실행(`yarn twenty dev:function:exec`)
HTTP, 크론, 데이터베이스 이벤트로 트리거하지 않고 로직 함수를 수동으로 실행하세요:
```bash filename="Terminal"
# Execute by function name
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the post-install function
yarn twenty dev:function:exec --postInstall
```
## 함수 로그 보기(`yarn twenty dev:function:logs`)
앱의 로직 함수 실행 로그를 스트리밍합니다:
```bash filename="Terminal"
# Stream all function logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
이는 Docker 컨테이너 로그를 표시하는 `yarn twenty docker:logs`와는 다릅니다. `yarn twenty dev:function:logs`는 Twenty 서버에서 앱의 함수 실행 로그를 표시합니다.
</Note>
## 타입이 지정된 클라이언트(`yarn twenty dev:generate-client`) 생성하기
앱을 빌드하거나 동기화하지 않고, 활성 원격의 스키마로부터 타입이 지정된 API 클라이언트(`twenty-client-sdk`)를 다시 생성합니다. 이를 사용해 별도 리포지토리에 존재하는 백엔드 서비스처럼, Twenty 인스턴스와 통신하는 어떤 프로젝트에서든 타입이 지정된 클라이언트를 얻을 수 있습니다:
```bash filename="Terminal"
# In your project (no Twenty app definition required)
yarn add twenty-sdk twenty-client-sdk
# Connect to the Twenty instance to generate the client from
yarn twenty remote:add
# Generate the typed client into node_modules/twenty-client-sdk
yarn twenty dev:generate-client
```
그런 다음 코드에서 클라이언트를 임포트하세요:
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
```
데이터 모델이 변경될 때마다 생성된 타입을 갱신하려면 명령을 다시 실행하세요.
<Note>
클라이언트는 `node_modules` 내부에서 생성되므로, 코드와 함께 커밋되지 않습니다. 매번 설치 후(`postinstall` 스크립트나 CI 등) `yarn twenty dev:generate-client`를 실행하세요.
</Note>
## 앱 제거(`yarn twenty app:uninstall`)
활성 워크스페이스에서 앱을 제거하세요:
```bash filename="Terminal"
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty app:uninstall --yes
```
## 리모트 관리
**리모트**는 앱이 연결하는 Twenty 서버를 의미합니다. 설정 과정에서 스캐폴더가 자동으로 하나를 생성합니다. 언제든지 리모트를 추가하거나 전환할 수 있습니다.
```bash filename="Terminal"
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote:add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote:add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote:list
# Set the active remote
yarn twenty remote:use <name>
```
자격 증명은 `~/.twenty/config.json`에 저장됩니다.
@@ -0,0 +1,32 @@
---
title: 개요
description: 앱을 빌드하고, 테스트하며, 배포하세요 — CLI 명령, 통합 테스트, CI, 그리고 서버 또는 npm으로의 게시까지 포함됩니다.
icon: rocket
---
\*\*운영 계층(operations layer)\*\*은 앱을 *사용하는 것*이 아니라 앱에 *수행하는* 모든 작업을 의미합니다. 예를 들어 CLI 명령 실행, 실제 Twenty 서버를 대상으로 한 통합 테스트 실행, CI 구성, 그리고 단일 서버에 배포되는 tarball로 또는 마켓플레이스에 등록되는 npm 패키지로 릴리스를 배포하는 작업 등이 여기에 포함됩니다.
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## 이 섹션에서 다루는 내용
<CardGroup cols={2}>
<Card title="CLI" icon="터미널" href="/l/ko/developers/extend/apps/operations/cli">
`yarn twenty` 참고 — exec, logs, uninstall, remotes.
</Card>
<Card title="동기화 및 복구" icon="나침반" href="/l/ko/developers/extend/apps/operations/sync-and-recovery">
동기화 차분을 읽을 때 어떤 명령을 사용할지와 복구 단계에 대해 다룹니다.
</Card>
<Card title="테스트" icon="flask" href="/l/ko/developers/extend/apps/operations/testing">
Vitest 설정, 통합 테스트, 타입 체크, CI 워크플로.
</Card>
<Card title="게시" icon="업로드" href="/l/ko/developers/extend/apps/operations/publishing">
빌드, tarball 배포, npm 게시, 설치.
</Card>
</CardGroup>
@@ -0,0 +1,294 @@
---
title: 게시
icon: 업로드
description: Twenty 앱을 마켓플레이스에 배포하거나 내부에 배포하세요.
---
## 개요
앱을 [로컬에서 빌드하고 테스트](/l/ko/developers/extend/apps/getting-started/concepts)하면, 배포 경로는 두 가지입니다:
* **타르볼 배포** — 내부 또는 비공개 사용을 위해 특정 Twenty 서버에 앱을 직접 업로드합니다.
* **npm에 게시** — 모든 워크스페이스가 검색하고 설치할 수 있도록 Twenty 마켓플레이스에 앱을 등재합니다.
두 경로는 동일한 **build** 단계에서 시작합니다.
## 앱 빌드
앱을 컴파일하고 배포 준비가 된 `manifest.json`을 생성하려면 빌드 명령을 실행하세요:
```bash filename="Terminal"
yarn twenty dev:build
```
이는 TypeScript 소스를 컴파일하고, 로직 함수와 프런트 컴포넌트를 트랜스파일하며, 모든 항목을 `.twenty/output/`에 기록합니다. 수동 배포 또는 publish 명령을 위한 `.tgz` 패키지도 생성하려면 `--tarball`을 추가하세요.
## 서버에 배포(타르볼)
공개하고 싶지 않은 앱 — 독점 도구, 엔터프라이즈 전용 통합 또는 실험적 빌드 — 의 경우, 타르볼을 Twenty 서버에 직접 배포할 수 있습니다.
### 사전 준비
배포 전에 대상 서버를 가리키도록 구성된 원격이 필요합니다. 원격은 서버 URL과 인증 자격 증명을 로컬의 `~/.twenty/config.json`에 저장합니다.
원격 추가:
```bash filename="Terminal"
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### 배포
앱을 한 번에 빌드하여 서버에 업로드:
```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --remote production
```
### 배포된 앱 공유
<Warning>
워크스페이스 간에 비공개(타르볼) 앱을 공유하는 것은 **엔터프라이즈** 기능입니다. 워크스페이스에 유효한 엔터프라이즈 키가 있을 때까지 **Distribution** 탭에는 공유 컨트롤 대신 업그레이드 프롬프트가 표시됩니다. 이를 활성화하려면 [설정 > 관리자 패널 > 엔터프라이즈](/settings/admin-panel#enterprise)로 이동하세요.
</Warning>
타르볼 앱은 공개 마켓플레이스에 나열되지 않으므로, 동일한 서버의 다른 워크스페이스는 탐색만으로는 이를 찾을 수 없습니다. 워크스페이스가 Enterprise 플랜을 사용 중이면, 배포된 앱을 다음과 같이 공유할 수 있습니다:
1. **설정 > 애플리케이션 > 등록**으로 이동하여 앱을 엽니다
2. **Distribution** 탭에서 **공유 링크 복사**를 클릭합니다
3. 이 링크를 다른 워크스페이스의 사용자와 공유하세요 — 해당 앱의 설치 페이지로 바로 이동합니다
공유 링크는 서버의 기본 URL(워크스페이스 하위 도메인 없이)을 사용하므로 서버의 모든 워크스페이스에서 동작합니다.
### 버전 관리
이미 배포된 타르볼 앱을 업데이트할 때, 서버는 `package.json`의 `version`이 현재 배포된 버전보다 [semver](https://semver.org) 순서 기준으로 엄격히 더 높아야 합니다. 동일한 버전을 다시 배포하거나 더 낮은 버전을 푸시하는 경우, 타르볼이 저장되기 전에 거부됩니다 — CLI에서 `VERSION_ALREADY_EXISTS` 오류가 표시됩니다.
업데이트를 릴리스하려면:
1. `package.json`의 `version` 필드를 올리세요(예: `1.2.3` → `1.2.4`, `1.3.0`, 또는 `2.0.0`)
2. `yarn twenty app:publish --private`(또는 `yarn twenty app:publish --private --remote production`)를 실행합니다.
3. 앱을 설치한 워크스페이스는 설정에서 사용 가능한 업그레이드를 확인할 수 있습니다
<Note>
프리릴리스 태그는 예상대로 동작합니다: `1.0.0-rc.1` → `1.0.0-rc.2`로 올리는 것은 허용되며, `1.0.0`과 같은 최종 릴리스는 `1.0.0-rc.5`보다 더 높은 버전으로 올바르게 인식됩니다. `package.json`의 버전은 유효한 semver 문자열이어야 합니다.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### 서버 버전 호환성
앱이 특정 Twenty 서버 버전에서 도입된 기능(예: v2.3.0에 추가된 OAuth 공급자)을 사용한다면, `package.json`의 `engines.twenty` 필드를 사용하여 앱에 필요한 최소 서버 버전을 선언해야 합니다:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
값은 표준 [semver 범위](https://github.com/npm/node-semver#ranges)입니다. 일반적인 패턴:
| 범위 | 의미 |
| ---------------------------------- | -------------------------------------- |
| `>=2.3.0` | 2.3.0 이상인 모든 서버 |
| `>=2.3.0 \<3.0.0` | 2.3.0 이상이지만 다음 메이저 버전 미만 |
| `^2.3.0` | `>=2.3.0 \<3.0.0`과 동일 |
**배포 및 설치 시 동작:**
* `engines.twenty`가 설정되어 있고 대상 서버의 버전이 해당 범위를 충족하지 않으면, 배포(tarball 업로드) 또는 설치가 `SERVER_VERSION_INCOMPATIBLE` 오류와 함께 거부되며 필요한 범위와 실제 서버 버전을 모두 나타내는 메시지가 표시됩니다.
* `engines.twenty`가 **설정되어 있지 않으면**, 앱은 모든 서버 버전에서 허용됩니다(기존 앱과 하위 호환).
* 서버에 `APP_VERSION`이 구성되어 있지 않으면, 검사가 생략됩니다.
<Note>
서버가 최종 검증을 수행합니다 — 타르볼 업로드와 워크스페이스 설치 모두에서 `engines.twenty`를 검증합니다. 타르볼을 별도 경로로 배포하거나 마켓플레이스에서 설치하더라도, 서버는 여전히 호환성을 강제합니다.
</Note>
## 자동화된 CI/CD (스캐폴드된 워크플로)
`create-twenty-app`으로 생성된 앱은 `.github/workflows/` 아래에 기본으로 두 개의 GitHub Actions 워크플로가 포함되어 있습니다. 저장소를 GitHub에 푸시하는 즉시 실행할 준비가 되어 있습니다 — CI에는 추가 설정이 필요 없고, CD에는 단 하나의 시크릿만 필요합니다.
### CI — `ci.yml`
`main`으로의 푸시와 풀 리퀘스트마다 통합 테스트를 자동으로 실행합니다.
**하는 일:**
1. 앱의 소스 코드를 체크아웃합니다.
2. `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` 컴포지트 액션을 사용해 격리된 Twenty 테스트 인스턴스를 생성합니다(CI에서 `yarn twenty docker:start --test`와 동등합니다).
3. Corepack을 활성화하고, `.nvmrc`에 따라 Node.js를 설정하며, `yarn install --immutable`로 의존성을 설치합니다.
4. 생성된 인스턴스에서 `TWENTY_API_URL`과 `TWENTY_API_KEY`를 전달하여 실제 서버와 통신할 수 있도록 `yarn test`를 실행합니다.
**구성 옵션:**
* `TWENTY_VERSION`(환경 변수, 기본값 `latest`) — `ci.yml`에서 이 값을 수정하여 CI에서 사용하는 Twenty 서버 버전을 고정합니다.
* 동시 실행은 `github.ref`로 그룹화되며, 새 푸시가 발생하면 진행 중인 실행을 취소합니다.
시크릿이 필요하지 않습니다 — 테스트 인스턴스는 일시적이며 작업이 진행되는 동안에만 존재합니다.
### CD — `cd.yml`
매번 `main`으로 푸시될 때 구성된 Twenty 서버에 앱을 배포하며, `deploy` 라벨이 적용된 경우 풀 리퀘스트에서도 선택적으로 배포합니다.
**하는 일:**
1. 라벨이 지정된 PR의 경우 PR 헤드, 아니면 푸시된 커밋을 체크아웃합니다.
2. `twentyhq/twenty/.github/actions/deploy-twenty-app@main`를 실행합니다 — CI에서의 `yarn twenty app:publish --private`에 해당합니다.
3. `twentyhq/twenty/.github/actions/install-twenty-app@main`를 실행하여 새로 배포된 버전을 대상 워크스페이스에 설치합니다.
**필수 구성:**
| 설정 | 위치 | 목적 |
| ----------------------- | --------------------------------------------------------- | ------------------------------------------------ |
| `TWENTY_DEPLOY_URL` | `cd.yml`의 `env`(기본값 `http://localhost:3000`) | 배포 대상 Twenty 서버입니다. 처음 사용하기 전에 실제 서버 URL로 변경하세요. |
| `TWENTY_DEPLOY_API_KEY` | GitHub 저장소 **Settings → Secrets and variables → Actions** | 대상 서버에서 배포 권한이 있는 API 키입니다. |
<Note>
기본 `TWENTY_DEPLOY_URL` 값 `http://localhost:3000`은 플레이스홀더이며 — GitHub 호스팅 러너에서는 아무 곳에도 도달하지 않습니다. CD를 활성화하기 전에 서버의 퍼블릭 URL로 업데이트하세요(또는 네트워크에 접근 가능한 셀프 호스티드 러너를 사용하세요).
</Note>
**PR에서 프리뷰 배포 트리거하기:**
풀 리퀘스트에 `deploy` 라벨을 추가하세요. `cd.yml`의 `if:` 가드는 해당 PR의 헤드 커밋을 사용해 그 PR에 대한 잡을 실행하므로, 머지 전에 대상 서버에서 변경 사항을 검증할 수 있습니다.
### 재사용 가능한 액션 고정하기
두 워크플로 모두 `@main`의 재사용 가능한 액션을 참조하므로, `twentyhq/twenty` 저장소의 액션 업데이트가 자동으로 반영됩니다. 결정적 빌드를 원한다면, 각 `uses:` 줄에서 `@main`을 커밋 SHA 또는 릴리스 태그로 바꾸세요.
## npm에 게시
npm에 게시하면 Twenty 마켓플레이스에서 앱을 찾을 수 있게 됩니다. 모든 Twenty 워크스페이스는 UI에서 바로 마켓플레이스 앱을 탐색, 설치 및 업그레이드할 수 있습니다.
### 요구 사항
* [npm](https://www.npmjs.com) 계정
* `package.json`의 `keywords` 배열에 있는 `twenty-app` 키워드(수동으로 추가하세요 — `create-twenty-app` 템플릿에 기본적으로 포함되어 있지 않습니다)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"]
}
```
### 마켓플레이스 메타데이터
`defineApplication()` 구성은 마켓플레이스에서 앱이 표시되는 방식을 제어하는 선택적 필드를 지원합니다. `public/` 폴더의 이미지를 참조하려면 `logoUrl`과 `screenshots`를 사용하세요:
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logoUrl: 'public/logo.png',
screenshots: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
마켓플레이스 필드 전체 목록(`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl` 등)은 Building Apps 페이지의 [defineApplication 아코디언](/l/ko/developers/extend/apps/config/application#marketplace-metadata)을 참조하세요.
#### 권장 스크린샷 크기
마켓플레이스는 `screenshots`를 고정된 `8:5` 컨테이너에 렌더링합니다(예: `1600×1000 px`).
<Note>
어떤 종횡비의 스크린샷이든 전체가 표시되며 잘리지 않습니다. 그러나 `8:5`보다 훨씬 세로로 길거나 가로로 좁은 경우 양쪽에 빈 띠가 표시됩니다.
</Note>
### 게시
```bash filename="Terminal"
yarn twenty app:publish
```
특정 dist-tag(예: `beta` 또는 `next`)로 게시하려면:
```bash filename="Terminal"
yarn twenty app:publish --tag beta
```
### 마켓플레이스 검색 방식
Twenty 서버는 npm 레지스트리에서 마켓플레이스 카탈로그를 **매시간** 동기화합니다.
기다리지 않고 즉시 동기화를 트리거할 수 있습니다:
```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```
마켓플레이스에 표시되는 메타데이터는 `defineApplication()` 구성에서 가져옵니다 — `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, `termsUrl` 같은 필드입니다.
<Note>
앱에서 `defineApplication()`에 `aboutDescription`을 정의하지 않으면, 마켓플레이스는 소개 페이지 콘텐츠로 npm에 게시된 패키지의 `README.md`를 자동으로 사용합니다. 즉, npm과 Twenty 마켓플레이스 모두에서 하나의 README만 관리하면 됩니다. 마켓플레이스에서 다른 설명을 사용하려면 `aboutDescription`을 명시적으로 설정하세요.
</Note>
### CI 게시
모든 릴리스마다 자동으로 게시하려면 이 GitHub Actions 워크플로를 사용하세요([OIDC](https://docs.npmjs.com/trusted-publishers) 사용):
```yaml filename=".github/workflows/publish.yml"
name: Publish
on:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: https://registry.npmjs.org
- run: yarn install --immutable
- run: npx twenty dev:build
- run: npm publish --provenance --access public
working-directory: .twenty/output
```
다른 CI 시스템(GitLab CI, CircleCI 등)에서도 동일한 세 가지 명령을 사용합니다: `yarn install`, `yarn twenty dev:build`, 그리고 `.twenty/output`에서 `npm publish`.
<Note>
**npm provenance**는 선택 사항이지만 권장됩니다. `--provenance` 옵션으로 게시하면 npm 목록에 신뢰 배지가 추가되어, 사용자가 공개 CI 파이프라인의 특정 커밋에서 패키지가 빌드되었는지 확인할 수 있습니다. 설정 방법은 [npm provenance 문서](https://docs.npmjs.com/generating-provenance-statements)를 참고하세요.
</Note>
## 앱 설치
앱이 게시(npm)되었거나 배포(타르볼)되면, 워크스페이스는 UI를 통해 이를 설치할 수 있습니다.
Twenty UI의 **설정 > 애플리케이션** 페이지로 이동하면 마켓플레이스 앱과 타르볼로 배포된 앱을 모두 탐색하고 설치할 수 있습니다.
{/* TODO: add screenshot of the UI when the app is registered */}
명령줄에서 앱을 설치할 수도 있습니다:
```bash filename="Terminal"
yarn twenty app:install
```
<Note>
서버는 설치 시 semver 버전 규칙을 강제하며, 배포와 동일한 규칙을 적용합니다:
* 워크스페이스에 이미 설치된 것과 동일한 버전을 설치하려고 하면 `APP_ALREADY_INSTALLED` 오류와 함께 거부됩니다.
* 현재 설치된 것보다 낮은 버전을 설치하려고 하면 `CANNOT_DOWNGRADE_APPLICATION` 오류와 함께 거부됩니다.
더 최신 버전을 설치하려면 먼저 배포하거나 게시한 다음 `yarn twenty app:install`을 다시 실행하세요.
</Note>
@@ -0,0 +1,111 @@
---
title: 동기화 및 복구
description: 언제 어떤 명령을 사용할지, 동기화 출력 결과를 읽는 방법, 로컬 메타데이터가 완전 초기화에 이르기 전에 드리프트할 때를 위한 복구 단계별 절차를 다룹니다.
icon: 나침반
---
로컬 앱 개발은 **동기화**를 중심으로 이루어집니다. CLI는 매니페스트를 다시 빌드하고, 서버는 워크스페이스에 이미 있는 메타데이터와의 차이만 적용합니다. 이 페이지에서는 어떤 명령을 사용할지, 동기화로 무엇이 변경되었는지 읽는 방법, 그리고 로컬 상태가 일관되지 않아 보일 때 순서대로 무엇을 해야 하는지를 설명합니다.
## 언제 어떤 명령을 사용할지
<Note>
일상적인 로컬 반복 개발에서는 거의 항상 `yarn twenty dev`를 사용하면 됩니다. 배포와 게시(publish)는 릴리스를 배포할 때 사용하는 것이며, 로컬 개발 루프용이 **아닙니다**.
</Note>
| 원하는 작업… | 명령 | 노트 |
| ---------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| 라이브 동기화로 로컬에서 반복 개발 | `yarn twenty dev` | 파일을 감시하고 변경될 때마다 동기화합니다. |
| 한 번만 동기화하고 종료 (CI, 스크립트, 훅) | `yarn twenty dev --once` | 한 번 빌드 + 동기화한 뒤 종료합니다. |
| 변경 사항을 **실제로 적용하지 않고** 미리 보기 | `yarn twenty dev --once --dry-run` | 차이를 계산해 출력만 하고, 아무것도 기록하지 않습니다. |
| 워크스페이스에서 앱 제거 | `yarn twenty app:uninstall` | 프롬프트를 건너뛰려면 `--yes`를 추가하세요. |
| 타르볼을 서버로 전송 | `yarn twenty app:publish --private` | `package.json`의 버전이 **엄격하게 더 높아야** 합니다. 자세한 내용은 [Publishing](/l/ko/developers/extend/apps/operations/publishing)을 참고하세요. |
| 마켓플레이스(npm)에 게시 | `yarn twenty app:publish` | — |
| 배포된 버전 설치 / 업그레이드 | `yarn twenty app:install` | 현재 배포된 버전을 설치합니다. |
| 로컬 서버를 초기화하고 깨끗하게 시작 | `yarn twenty docker:reset` | 로컬 데이터 **전체**를 삭제합니다. 최후의 수단입니다. |
### 로컬 동기화에는 버전 증가가 필요 없음
엄격히 증가하는 `version` 규칙(`deploy` 시 `VERSION_ALREADY_EXISTS`, `install` 시 `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION`)은 **`app:publish` / `app:install`**, 즉 릴리스 경로에만 적용됩니다. `yarn twenty dev`는 매니페스트를 제자리에서 동기화하므로 버전을 변경할 필요가 없습니다. 따라서 반복 개발을 위해 `package.json`을 수정할 필요가 없습니다. 로컬 변경을 테스트하기 위해 버전을 올리고 있다면, 개발 루프가 아니라 릴리스 경로를 사용하고 있는 것입니다.
## 동기화 출력 읽기
각 동기화는 적용된(또는 `--dry-run`일 경우 적용될) 메타데이터 변경 사항을 출력합니다.
```text filename="Terminal"
Metadata changes: 2 created, 1 updated, 1 deleted
created objectMetadata rocket
created fieldMetadata timelineActivities
updated fieldMetadata launchedAt
deleted pageLayout legacyTab
✓ Synced
```
이 출력이 1차 진단 도구입니다. 어떤 객체, 필드, 레이아웃이 변경되었는지 정확히 알려주므로, UI를 확인하기 전에 동기화가 예상대로 동작했는지 검증할 수 있습니다.
동기화가 단일 엔티티에서 실패하면, 오류 메시지에 문제의 엔티티와 그 `universalIdentifier`가 함께 표시됩니다. 예를 들면 다음과 같습니다.
```text
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
그 식별자를 사용해 매니페스트(필요하다면 워크스페이스)에서 해당 엔티티를 찾아, 어떤 것이 충돌하는지 추측하지 말고 정확히 확인하세요.
## 변경 사항 미리 보기(dry run)
`yarn twenty dev --once --dry-run`은 매니페스트를 빌드하고, 서버에 마이그레이션 계획을 요청한 뒤, 이를 출력만 합니다. **아무것도 실제로 적용하지 않습니다**. "이 동기화로 무엇이 바뀔까?"에 안전하게 답할 수 있는 방법으로, 실제로 적용하기 전에 확인할 수 있습니다.
```bash filename="Terminal"
yarn twenty dev --once --dry-run
```
```text filename="Terminal"
Building manifest...
Computing metadata diff (dry run, nothing will be applied)...
Metadata changes: 1 created, 1 updated
created fieldMetadata timelineActivities
updated objectMetadata rocket
✓ Dry run complete for My App — no changes were applied
```
dry run은 다음과 같습니다.
* **아무것도 기록하지 않습니다**. 메타데이터 마이그레이션, 애플리케이션 레코드 업데이트, 기본 역할/탭 변경, API 클라이언트 생성이 모두 수행되지 않습니다.
* 실제 동기화에서 적용될 **동일한 diff**를 반환하므로, 생성/업데이트/삭제되는 엔티티를 미리 검토할 수 있습니다.
* 위험한 변경 전에, AI가 생성한 변경 사항을 검토할 때, 또는 예기치 않은 변경이 적용되려 하면 실패해야 하는 스크립트에서 유용합니다.
<Note>
dry run은 **메타데이터** 변경만 미리 보여 주며, 앱이 최소 한 번 이상 동기화된 상태(워크스페이스가 이 앱을 알고 있는 상태)여야 합니다. 한 번도 동기화된 적이 없는 앱에 대해 dry run을 실행하면, 서버는 앱이 설치되지 않았다고 보고합니다. 먼저 `yarn twenty dev`를 한 번 실행하세요.
</Note>
## 복구 단계별 절차
로컬 메타데이터가 잘못된 것처럼 보일 때는, 아래 순서대로 단계를 진행하면서 문제가 해결되는 즉시 멈추세요. 각 단계는 이전 단계보다 더 많은 영향을 미칩니다.
1. **재동기화.** `yarn twenty dev --once`를 다시 실행하세요. 동기화는 멱등적이므로, 깨끗한 매니페스트를 다시 실행해도 안전하며 일시적인 오류가 이 방식으로 해결되는 경우가 많습니다.
2. **계획 미리 보기.** `yarn twenty dev --once --dry-run`을 실행해, 다음 동기화가 정확히 무엇을 변경하려 하는지 실제로 적용하지 않고 확인하세요.
3. **명시적인 오류 읽기.** 동기화가 실패하면, 메시지에 포함된 메타데이터 타입과 `universalIdentifier`(위 참조)를 확인한 뒤, 매니페스트에서 해당 엔티티를 찾으세요. 충돌은 보통 중복되었거나 재사용된 식별자를 가리킵니다.
4. **삭제 후 재설치.** `yarn twenty app:uninstall`을 실행한 뒤, 다시 동기화합니다(`yarn twenty dev`). 이 방법은 워크스페이스의 나머지 부분은 그대로 둔 채, 앱의 메타데이터를 깨끗한 상태에서 다시 구축합니다.
5. **전체 초기화(최후의 수단).** `yarn twenty docker:reset`을 실행한 뒤, 다시 시드하고 재동기화합니다.
<Warning>
`yarn twenty docker:reset`은 로컬 인스턴스의 **모든** 데이터를 삭제합니다. 모든 워크스페이스, 레코드, 앱이 제거됩니다. 이전 단계들이 모두 실패했을 때에만 사용하세요.
</Warning>
<Note>
메타데이터 오류가 발생했나요? [이슈를 생성](https://github.com/twentyhq/twenty/issues/new/choose)해 주세요. 이때 실패한 마이그레이션 메시지(메타데이터 타입과 `universalIdentifier` 포함), 동기화 시 출력된 `Metadata changes`, 그리고 실행한 명령들을 함께 첨부해 주세요.
</Note>
## 하나의 워크스페이스에서 동시 동기화 피하기
동기화는 메타데이터 마이그레이션을 적용합니다. **동일한 워크스페이스에 대해 동시에** 여러 번 동기화, 배포, 설치 작업을 실행하면(예: 여러 터미널이나 AI 에이전트가 병렬로 반복 실행하는 경우), 마이그레이션이 서로 얽혀 메타데이터가 부분적으로만 적용된 상태로 남을 수 있습니다.
서버는 이를 방지하기 위해 워크스페이스별로 동기화를 직렬화하지만, 그럼에도 중요한 메타데이터 작업은 동시에 실행하지 말고 **단일** 프로세스를 통해 순차적으로 실행하는 것이 좋습니다. 여러 에이전트로 개발을 오케스트레이션한다면, 이들의 sync/deploy/install 호출을 하나의 큐로 모아 한 번에 하나씩만 실행되도록 하세요.
## 실패 유형 구분하기
문제가 발생했을 때, 메타데이터 diff와 명시적인 오류를 통해 실패 지점을 파악할 수 있습니다.
* **매니페스트 빌드 오류** — 동기화 전에 CLI가 실패합니다(`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`). 앱 소스를 수정하세요.
* **동기화 / 마이그레이션 오류** — 빌드는 성공했지만 diff를 적용하는 데 실패하며, 메시지에 엔티티와 `universalIdentifier`가 표시됩니다. 충돌하는 메타데이터를 수정하세요.
* **앱 코드 런타임 오류** — 동기화는 성공했지만 로직 함수나 컴포넌트가 런타임에 올바르게 동작하지 않는 경우입니다. [function logs](/l/ko/developers/extend/apps/operations/cli)를 확인하세요.
* **로컬 인스턴스 상태** — 위의 어느 경우에도 해당하지 않지만 워크스페이스가 여전히 잘못되어 보이는 경우입니다. 복구 사다리를 순서대로 따라가세요.
@@ -0,0 +1,301 @@
---
title: 테스트
description: Vitest 설정, 실제 Twenty 서버를 대상으로 한 통합 테스트, 타입 검사, 그리고 GitHub Actions를 사용하는 CI.
icon: flask
---
SDK는 테스트 코드에서 앱을 빌드, 배포, 설치 및 제거할 수 있는 프로그래매틱 API를 제공합니다. 이것을 [Vitest](https://vitest.dev/) 및 타입이 지정된 API 클라이언트와 함께 사용하면 실제 Twenty 서버를 대상으로 앱이 종단 간으로 동작하는지 검증하는 통합 테스트를 작성할 수 있습니다.
## npm 패키지 사용
앱에서 원하는 npm 패키지를 설치해 사용할 수 있습니다. 로직 함수와 프런트 컴포넌트는 모두 [esbuild](https://esbuild.github.io/)로 번들되며, 모든 의존성이 출력물에 인라인됩니다 — 런타임에는 `node_modules`가 필요하지 않습니다.
### 패키지 설치
```bash filename="Terminal"
yarn add axios
```
그런 다음 코드에서 임포트하세요:
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
```
프런트 컴포넌트에서도 동일하게 작동합니다:
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
```
### 번들링 동작 방식
빌드 단계에서는 esbuild를 사용하여 로직 함수와 프런트 컴포넌트마다 단일 자체 포함 파일을 생성합니다. 모든 임포트된 패키지는 번들에 인라인됩니다.
**로직 함수**는 Node.js 환경에서 실행됩니다. Node 기본 모듈(`fs`, `path`, `crypto`, `http` 등) 을(를) 사용할 수 있으며 설치할 필요가 없습니다.
**프런트 컴포넌트**는 Web Worker에서 실행됩니다. Node 기본 모듈은 사용할 수 없습니다 — 브라우저 환경에서 동작하는 브라우저 API와 npm 패키지만 사용할 수 있습니다.
두 환경 모두에서 `twenty-client-sdk/core`와 `twenty-client-sdk/metadata`가 사전 제공 모듈로 사용 가능합니다 — 이는 번들되지 않고 서버가 런타임에 해석합니다.
## 설정
스캐폴딩된 앱에는 이미 Vitest가 포함되어 있습니다. 수동으로 설정하는 경우, 의존성을 설치하세요:
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
앱 루트에 `vitest.config.ts`를 생성하세요:
```ts vitest.config.ts
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: 'http://localhost:2020',
TWENTY_API_KEY: 'your-api-key',
},
},
});
```
테스트 실행 전에 서버에 접근 가능한지 확인하는 설정 파일을 생성하세요:
```ts src/__tests__/setup-test.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
beforeAll(async () => {
// Verify the server is running
const response = await fetch(`${TWENTY_API_URL}/healthz`);
if (!response.ok) {
throw new Error(
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
'Start the server before running integration tests.',
);
}
// Write a temporary config for the SDK
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify({
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
}, null, 2),
);
});
```
## 프로그래매틱 SDK API
`twenty-sdk/cli` 서브 경로는 테스트 코드에서 직접 호출할 수 있는 함수를 내보냅니다:
| 함수 | 설명 |
| -------------- | --------------------- |
| `appBuild` | 앱을 빌드하고 필요하면 타르볼로 패키징 |
| `appDeploy` | 타르볼을 서버로 업로드 |
| `appInstall` | 활성 워크스페이스에 앱 설치 |
| `appUninstall` | 활성 워크스페이스에서 앱 제거 |
각 함수는 `success: boolean`과 `data` 또는 `error`를 포함한 결과 객체를 반환합니다.
## 통합 테스트 작성
다음은 앱을 빌드, 배포, 설치한 후 워크스페이스에 표시되는지 검증하는 전체 예제입니다:
```ts src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
```
## 테스트 실행
로컬 Twenty 서버가 실행 중인지 확인한 다음, 다음을 실행하세요:
```bash filename="Terminal"
yarn test
```
또는 개발 중에는 워치 모드로 실행하세요:
```bash filename="Terminal"
yarn test:watch
```
## 타입 검사
테스트를 실행하지 않고도 앱에 대해 타입 검사를 수행할 수 있습니다:
```bash filename="Terminal"
yarn twenty dev:typecheck
```
이는 `tsc --noEmit`를 실행하고 모든 타입 오류를 보고합니다.
## GitHub Actions로 CI
스캐폴더가 `.github/workflows/ci.yml`에 바로 사용할 수 있는 GitHub Actions 워크플로를 생성합니다. `main`으로의 푸시와 풀 리퀘스트마다 통합 테스트를 자동으로 실행합니다.
워크플로:
1. 코드를 체크아웃합니다
2. `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` 액션을 사용해 임시 Twenty 서버를 구동합니다
3. `yarn install --immutable`로 종속성을 설치합니다
4. 액션 출력에서 주입된 `TWENTY_API_URL` 및 `TWENTY_API_KEY`로 `yarn test`를 실행합니다
```yaml .github/workflows/ci.yml
name: CI
on:
push:
branches:
- main
pull_request: {}
env:
TWENTY_VERSION: latest
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Spawn Twenty instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: Run integration tests
run: yarn test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
```
별도의 시크릿을 구성할 필요가 없습니다 — `spawn-twenty-docker-image` 액션이 러너 내에서 일시적인 Twenty 서버를 직접 시작하고 연결 정보를 출력합니다. `GITHUB_TOKEN` 시크릿은 GitHub에서 자동으로 제공됩니다.
`latest` 대신 특정 Twenty 버전을 고정하려면 워크플로 상단의 `TWENTY_VERSION` 환경 변수를 변경하세요.
@@ -17,7 +17,7 @@ Twenty는 귀하의 데이터 모델에 맞는 API를 특별히 생성합니다:
* **맞춤형 문서**: 작업 공간의 데이터 모델에 맞게 특별히 생성됩니다.
<Note>
맞춤형 API 문서는 API 키 생성 후 **설정 → API 및 웹훅**에서 확인할 수 있습니다. Twenty가 사용자 지정 데이터 모델에 맞는 API를 생성하므로, 문서는 귀하의 워크스페이스에 고유합니다.
맞춤형 API 문서는 API 키 생성 후 **설정 → API 및 웹훅**에서 확인할 수 있습니다. Twenty가 사용자 지정 데이터 모델에 맞는 API를 생성하므로, 문서는 귀하의 워크스페이스에 고유합니다.
</Note>
## 두 가지 API 유형
@@ -81,14 +81,14 @@ Authorization: Bearer YOUR_API_KEY
<VimeoEmbed videoId="928786722" title="API 키 생성" />
<Warning>
API 키는 민감한 데이터에 대한 액세스를 부여합니다. 신뢰할 수 없는 서비스와 공유하지 마세요. 유출되었으면 즉시 비활성화하고 새 키를 생성하세요.
API 키는 민감한 데이터에 대한 액세스를 부여합니다. 신뢰할 수 없는 서비스와 공유하지 마세요. 유출되었으면 즉시 비활성화하고 새 키를 생성하세요.
</Warning>
### API 키에 역할 할당
보안을 강화하려면 액세스를 제한할 특정 역할을 할당하세요:
1. **설정 → 역할**로 이동
1. **설정 → 구성원 → 역할**로 이동하세요
2. 할당할 역할을 클릭하세요
3. **배정** 탭 열기
4. **API 키**에서 **+ API 키에 할당**을 클릭하세요
@@ -143,5 +143,5 @@ REST와 GraphQL 모두 배치 작업을 지원합니다:
| **배치 크기** | 호출당 기록 60개 |
<Tip>
처리량을 극대화하려면 배치 작업을 사용하세요 — 개별 요청 대신 단일 API 호출로 최대 60개의 기록을 처리할 수 있습니다.
처리량을 극대화하려면 배치 작업을 사용하세요 — 개별 요청 대신 단일 API 호출로 최대 60개의 기록을 처리할 수 있습니다.
</Tip>
@@ -62,7 +62,7 @@ Twenty는 다음 이벤트 유형에 대해 웹훅을 전송합니다:
| `타임스탬프` | 이벤트가 발생한 시각(UTC) |
<Note>
수신을 확인하기 위해 **2xx HTTP 상태**(200-299)로 응답하세요. 2xx가 아닌 응답은 전달 실패로 기록됩니다.
수신을 확인하기 위해 **2xx HTTP 상태**(200-299)로 응답하세요. 2xx가 아닌 응답은 전달 실패로 기록됩니다.
</Note>
## 웹훅 검증
@@ -1,7 +1,6 @@
---
title: 확장
description: API, 웹훅 및 맞춤형 앱으로 Twenty의 기능을 확장하세요.
redirect: /developers/introduction
---
<Frame>
@@ -16,20 +15,18 @@ Twenty는 확장 가능하도록 설계되었습니다. 당사의 API, 웹훅
* **API**: REST 또는 GraphQL을 사용해 프로그래밍 방식으로 CRM 데이터를 쿼리하고 수정하세요
* **웹훅**: Twenty에서 이벤트가 발생하면 실시간 알림을 받으세요
* **앱**: Twenty의 기능을 확장하는 맞춤형 앱을 구축하세요 - 곧 제공됩니다!
* **앱**: Twenty의 기능을 확장하는 맞춤형 앱을 구축하세요
## 시작하기
<CardGroup cols={2}>
<Card title="API" icon="코드" href="/l/ko/developers/api">
<Card title="API" icon="코드" href="/l/ko/developers/extend/api">
프로그래밍 방식으로 Twenty에 연결하세요
</Card>
<Card title="웹훅" icon="bell" href="/l/ko/developers/webhooks">
<Card title="웹훅" icon="bell" href="/l/ko/developers/extend/webhooks">
이벤트에 대한 실시간 알림을 받으세요
</Card>
<Card title="앱" icon="puzzle-piece" href="/l/ko/developers/apps/apps">
코드로 사용자 지정을 구축하세요(알파)
<Card title="앱" icon="puzzle-piece" href="/l/ko/developers/extend/apps/getting-started">
코드로 사용자 지정을 구축하세요
</Card>
</CardGroup>
@@ -0,0 +1,189 @@
---
title: OAuth
icon: 키
description: 서버 간 액세스를 위한 PKCE가 포함된 권한 부여 코드 플로우와 클라이언트 자격 증명.
---
Twenty는 사용자 대상 앱에는 권한 부여 코드 + PKCE를, 서버 간 액세스에는 클라이언트 자격 증명을 사용하여 OAuth 2.0을 구현합니다. 클라이언트는 [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)에 따라 동적으로 등록되며 — 대시보드에서 수동 설정은 필요하지 않습니다.
## OAuth 사용 시기
| 시나리오 | 인증 방식 |
| ------------------- | ---------------------------------------------------------------- |
| 내부 스크립트, 자동화 | [API 키](/l/ko/developers/extend/api#authentication) |
| 사용자를 대신하여 동작하는 외부 앱 | **OAuth — 권한 부여 코드** |
| 서버 간, 사용자 컨텍스트 없음 | **OAuth — 클라이언트 자격 증명** |
| UI 확장이 포함된 Twenty 앱 | [앱](/l/ko/developers/extend/apps/getting-started) (OAuth는 자동으로 처리됩니다) |
## 클라이언트 등록
Twenty는 [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)에 따라 **동적 클라이언트 등록**을 지원합니다. 수동 설정이 필요 없습니다 — 프로그래밍 방식으로 등록하세요:
```bash
POST /oauth/register
Content-Type: application/json
{
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_post"
}
```
**응답:**
```json
{
"client_id": "abc123",
"client_secret": "secret456",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"]
}
```
<Warning>
`client_secret`을 안전하게 저장하세요 — 나중에 다시 조회할 수 없습니다.
</Warning>
## 범위
| 범위 | 액세스 |
| ----- | ------------------------------------ |
| `api` | Core 및 Metadata API에 대한 전체 읽기/쓰기 액세스 |
| `프로필` | 인증된 사용자의 프로필 정보 읽기 |
범위를 공백으로 구분된 문자열로 요청: `scope=api profile`
## 권한 부여 코드 플로우
앱이 Twenty 사용자를 대신해 동작할 때 이 플로우를 사용하세요.
### 1. 사용자를 권한 부여 화면으로 리디렉션
```
GET /oauth/authorize?
client_id=YOUR_CLIENT_ID&
response_type=code&
redirect_uri=https://myapp.com/callback&
scope=api&
state=random_state_value&
code_challenge=CHALLENGE&
code_challenge_method=S256
```
| 매개변수 | 필수 | 설명 |
| ----------------------- | --- | ------------------------------------------ |
| `client_id` | 예 | 등록된 클라이언트 ID |
| `response_type` | 예 | 반드시 `code`여야 합니다 |
| `redirect_uri` | 예 | 등록된 리디렉션 URI와 일치해야 합니다 |
| `scope` | 아니요 | 공백으로 구분된 범위(기본값은 `api`) |
| `상태` | 권장 | CSRF 공격을 방지하기 위한 랜덤 문자열 |
| `code_challenge` | 권장 | PKCE 챌린지(검증자 값의 SHA-256 해시, base64url 인코딩) |
| `code_challenge_method` | 권장 | PKCE를 사용할 때는 `S256`이어야 합니다 |
사용자는 동의 화면을 보고 액세스를 승인하거나 거부합니다.
### 2. 콜백 처리
권한 부여 후, Twenty는 귀하의 `redirect_uri`로 다시 리디렉션합니다:
```
https://myapp.com/callback?code=AUTH_CODE&state=random_state_value
```
`state`가 보낸 값과 일치하는지 확인하세요.
### 3. 코드를 토큰으로 교환
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://myapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=YOUR_PKCE_VERIFIER
```
**응답:**
```json
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBh..."
}
```
### 4. 액세스 토큰 사용
```bash
GET /rest/companies
Authorization: Bearer ACCESS_TOKEN
```
### 5. 만료 시 갱신
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=YOUR_REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
```
## 클라이언트 자격 증명 플로우
사용자 상호작용이 없는 서버 간 통합의 경우:
```bash
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=api
```
반환된 토큰은 특정 사용자에 연결되지 않고 워크스페이스 수준의 액세스를 가집니다.
## 서버 디스커버리
Twenty는 표준 디스커버리 엔드포인트에서 OAuth 구성을 게시합니다:
```
GET /.well-known/oauth-authorization-server
```
이 엔드포인트는 모든 엔드포인트, 지원되는 그랜트 타입, 범위 및 기능을 반환하며 — 범용 OAuth 클라이언트를 구축하는 데 유용합니다.
## API 엔드포인트 요약
| 엔드포인트 | 목적 |
| ----------------------------------------- | -------------- |
| `/.well-known/oauth-authorization-server` | 서버 메타데이터 디스커버리 |
| `/oauth/register` | 동적 클라이언트 등록 |
| `/oauth/authorize` | 사용자 권한 부여 |
| `/oauth/token` | 토큰 교환 및 갱신 |
| 환경 | 기본 URL |
| ---------- | ------------------------ |
| **클라우드** | `https://api.twenty.com` |
| **셀프 호스팅** | `https://{your-domain}` |
## OAuth 대 API 키
| | API 키 | OAuth |
| ------------- | ------------- | ---------------- |
| **설정** | 설정에서 생성 | 클라이언트 등록, 플로우 구현 |
| **사용자 컨텍스트** | 없음(워크스페이스 수준) | 특정 사용자의 권한 |
| **적합한 용도** | 스크립트, 내부 도구 | 외부 앱, 다중 사용자 통합 |
| **토큰 로테이션** | 수동 | 리프레시 토큰으로 자동 |
| **범위 지정 액세스** | 전체 API 액세스 | 범위를 통해 세분화된 액세스 |
@@ -0,0 +1,117 @@
---
title: 웹훅
icon: satellite-dish
description: 레코드가 변경될 때 알림을 받으세요 — 생성, 업데이트, 삭제마다 귀하의 엔드포인트로 HTTP POST를 전송합니다.
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
레코드가 생성, 업데이트 또는 삭제될 때마다 Twenty가 귀하의 URL로 HTTP POST를 전송합니다. 사용자 지정 오브젝트를 포함한 모든 오브젝트 유형이 지원됩니다.
## 웹훅 생성
1. **설정 → API 및 웹훅 → 웹훅**으로 이동
2. **+ 웹훅 생성** 클릭
3. 웹훅 URL을 입력하세요(공개적으로 액세스 가능해야 함)
4. **저장**을 클릭합니다
웹훅이 즉시 활성화되어 알림 전송을 시작합니다.
<VimeoEmbed videoId="928786708" title="웹훅 생성" />
### 웹훅 관리
**편집**: 웹훅을 클릭 → URL 업데이트 → **저장**
**삭제**: 웹훅을 클릭 → **삭제** → 확인
## 이벤트
Twenty는 다음 이벤트 유형에 대해 웹훅을 전송합니다:
| 이벤트 | 예시 |
| ------------- | ---------------------------------------------------------- |
| **레코드 생성됨** | `person.created`, `company.created`, `note.created` |
| **레코드 업데이트됨** | `person.updated`, `company.updated`, `opportunity.updated` |
| **레코드 삭제됨** | `person.deleted`, `company.deleted` |
모든 이벤트 유형은 귀하의 웹훅 URL로 전송됩니다. 이벤트 필터링은 향후 릴리스에서 추가될 수 있습니다.
## 페이로드 형식
각 웹훅은 JSON 본문을 포함한 HTTP POST를 전송합니다:
```json
{
"event": "person.created",
"data": {
"id": "abc12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"createdAt": "2025-02-10T15:30:45Z",
"createdBy": "user_123"
},
"timestamp": "2025-02-10T15:30:50Z"
}
```
| 필드 | 설명 |
| ----------- | -------------------------------- |
| `event` | 무슨 일이 발생했는지(예: `person.created`) |
| `data` | 생성/업데이트/삭제된 전체 레코드 |
| `timestamp` | 이벤트가 발생한 시각(UTC) |
<Note>
수신을 확인하기 위해 **2xx HTTP 상태**(200-299)로 응답하세요. 2xx가 아닌 응답은 전달 실패로 기록됩니다.
</Note>
## 웹훅 검증
Twenty는 보안을 위해 각 웹훅 요청에 서명합니다. 요청의 진위를 보장하기 위해 서명을 검증하세요.
### 헤더
| 헤더 | 설명 |
| ---------------------------- | -------------- |
| `X-Twenty-Webhook-Signature` | HMAC SHA256 서명 |
| `X-Twenty-Webhook-Timestamp` | 요청 타임스탬프 |
### 검증 단계
1. `X-Twenty-Webhook-Timestamp`에서 타임스탬프를 가져옵니다
2. 다음 문자열을 생성합니다: `{timestamp}:{JSON payload}`
3. 웹훅 비밀을 사용하여 HMAC SHA256을 계산합니다
4. `X-Twenty-Webhook-Signature`와 비교합니다
### 예시(Node.js)
```javascript
const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const receivedSignature = req.headers["x-twenty-webhook-signature"];
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, "hex"),
Buffer.from(receivedSignature, "hex")
);
```
## 웹훅 vs 워크플로
| 방법 | 방향 | 사용 사례 |
| ------------------ | --- | ---------------------------------- |
| **웹훅** | OUT | 레코드 변경 사항을 외부 시스템에 자동으로 알립니다 |
| **워크플로 + HTTP 요청** | OUT | 사용자 지정 로직(필터, 변환)으로 데이터를 외부로 전송합니다 |
| **워크플로 웹훅 트리거** | IN | 외부 시스템에서 Twenty로 데이터를 수신합니다 |
외부 데이터를 수신하려면 [웹훅 트리거 설정](/l/ko/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger)을 참조하세요.
@@ -1,33 +1,28 @@
---
title: 시작하기
description: Twenty 개발자 문서에 오신 것을 환영합니다. 이 문서는 Twenty를 확장하고, 자체 호스팅하며, Twenty에 기여하는 데 필요한 자료를 제공합니다.
title: 개발자
description: 앱을 개발하거나, API를 사용하거나, 자체 호스팅하거나, 코드베이스에 기여하세요.
---
import { CardTitle } from "/snippets/card-title.mdx"
<CardGroup cols={2}>
<Card href="/l/ko/developers/api" icon="code">
<CardGroup cols={3}>
<Card href="/l/ko/developers/extend/apps/getting-started" img="/images/user-guide/halftone/dev-apps.png">
<CardTitle>앱</CardTitle>
커스텀 오브젝트, 서버 측 로직, UI 컴포넌트, AI 에이전트로 Twenty를 확장하세요 — 모두 TypeScript 패키지 형태입니다.
</Card>
<Card href="/l/ko/developers/extend/api" img="/images/user-guide/halftone/dev-api.png">
<CardTitle>API</CardTitle>
REST 또는 GraphQL로 CRM 데이터를 쿼리하고 수정합니다.
REST GraphQL API, 웹훅, 그리고 OAuth.
</Card>
<Card href="/l/ko/developers/webhooks" icon="bell">
<CardTitle>Webhooks</CardTitle>
이벤트 발생 시 실시간 알림을 받습니다.
</Card>
<Card href="/l/ko/developers/apps/apps" icon="puzzle-piece">
<CardTitle>Apps</CardTitle>
Twenty의 기능을 확장하는 맞춤형 애플리케이션을 구축하세요.
</Card>
<Card href="/l/ko/developers/self-host/self-host" icon="desktop">
<Card href="/l/ko/developers/self-host/capabilities/docker-compose" img="/images/user-guide/halftone/dev-self-host.png">
<CardTitle>자체 호스팅</CardTitle>
자체 인프라에 Twenty를 배포하고 관리하세요.
자체 인프라에 Twenty를 실행하세요.
</Card>
<Card href="/l/ko/developers/contribute/contribute" icon="github">
<Card href="/l/ko/developers/contribute/capabilities/local-setup" img="/images/user-guide/halftone/dev-contribute.png">
<CardTitle>기여</CardTitle>
오픈 소스 커뮤니티에 참여하고 Twenty에 기여하세요.
로컬에서 모노레포를 설정하고 PR을 제출하세요.
</Card>
</CardGroup>
@@ -1,9 +1,10 @@
---
title: Docker Compose로 1-클릭
title: Docker Compose
icon: docker
---
<Warning>
Docker 컨테이너는 프로덕션 호스팅 또는 셀프 호스팅을 위한 것입니다. 기여를 원하시면 [로컬 설정](/l/ko/developers/contribute/capabilities/local-setup)을 확인하세요.
Docker 컨테이너는 프로덕션 호스팅 또는 셀프 호스팅입니다. 기여하려면 [로컬 설정](/l/ko/developers/contribute/capabilities/local-setup)을 확인하세요.
</Warning>
## 개요
@@ -12,7 +13,7 @@ title: Docker Compose로 1-클릭
**중요:** 이 가이드에서 명시적으로 언급된 설정만 수정하세요. 다른 구성을 변경하면 문제가 발생할 수 있습니다.
고급 설정을 위해 [환경 변수 설정](/l/ko/developers/self-host/capabilities/setup) 문서를 참조하세요. 모든 환경 변수는 서버 및/또는 작업자 수준에서 docker-compose.yml 파일에 선언되어야 합니다.
고급 설정을 위해 [환경 변수 설정](/l/ko/developers/self-host/capabilities/setup) 문서를 참조하세요. 모든 환경 변수는 변수에 따라 서버 및/또는 작업자 수준에서 `docker-compose.yml` 파일에 선언되어야 합니다.
## 시스템 요구 사항
@@ -50,7 +51,7 @@ VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
```
2. **비밀 토큰 생성**
2. **암호화 키 생성하기**
고유한 랜덤 문자열을 생성하기 위해 다음 명령을 실행하세요:
@@ -58,16 +59,18 @@ VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.
openssl rand -base64 32
```
**중요:** 이 값을 비공개로 유지/공유하지 마십시오.
**중요:** 이 값을 비공개로 유지/공유하지 마십시오. `ENCRYPTION_KEY`를 잃어버리면 데이터베이스에 저장된 모든 비밀(예: OAuth 토큰, 애플리케이션 변수, TOTP 비밀 등)에 대한 액세스를 잃게 됩니다.
3. **`.env` 업데이트**
생성된 토큰으로 .env 파일의 플레이스홀더 값을 대체합니다.
```ini
APP_SECRET=first_random_string
ENCRYPTION_KEY=random_string
```
중단 없이 키를 회전하는 방법은 [키 회전 가이드](/l/ko/developers/self-host/capabilities/key-rotation)를 참조하세요.
4. **Postgres 비밀번호 설정**
.env 파일에서 특수 문자가 없는 강력한 비밀번호로 `PG_DATABASE_PASSWORD` 값을 업데이트하십시오.
@@ -0,0 +1,60 @@
---
title: 키 로테이션
icon: rotate
---
Twenty에는 서로 독립적인 키 계열이 두 가지 있습니다:
* **JWT 서명 키** — 비대칭 ES256 키 페어(`kid` 태그 사용)로, `core."signingKey"`에 저장되며 액세스/리프레시 토큰을 서명하고 검증하는 데 사용됩니다.
* **정지 상태 암호화 키** — OAuth 토큰, 애플리케이션 변수, 서명 키의 개인 키, 민감한 설정 값, `enc:v2:` 봉투 안의 TOTP 시크릿을 암호화하는 데 사용되는 `ENCRYPTION_KEY`입니다.
`APP_SECRET`은 하위 호환성을 위해 유지되는 레거시 시크릿입니다. `ENCRYPTION_KEY`가 설정되지 않은 경우 정지 상태 암호화/세션 쿠키 폴백 역할을 하며, 기존 HS256 액세스 토큰을 계속 검증합니다. 이는 사용 중단 예정입니다.
## JWT 서명 키
각 키는 `publicKey`(이전에 발급된 토큰을 검증할 수 있도록 무기한 보관), 암호화된 `privateKey`(해당 키가 현재 키일 때만 사용), `isCurrent` 플래그(한 번에 정확히 한 행만 true), 선택적인 `revokedAt`를 가집니다.
### 현재 키 회전
옵트인하려면 `SIGNING_KEY_ROTATION_DAYS` 를 설정하세요. 그러면 일일 cron 작업이 기존 키가 해당 기준값보다 오래되면 새로운 현재 키를 발급합니다. 이전 키는 *폐기되지 않으므로*, 해당 키로 서명된 토큰은 계속 검증됩니다. 자동 회전을 사용하지 않으려면 변수를 설정하지 않은 상태로 두세요.
<Note>자동 회전 기능은 v2.6+부터 제공됩니다.</Note>
### 키 폐기(유출 / 비상 시에만)
현재 키가 아닌 행에서 **Settings → Admin Panel → Signing keys → Revoke**를 수행합니다. 암호화된 개인 데이터를 삭제하고 `revokedAt`을 설정하며, 해당 `kid`로 서명된 모든 기존 토큰을 거부합니다.
## `ENCRYPTION_KEY` 회전
<Note>아래에 설명된 `secret-encryption:rotate` 커맨드는 v2.6+에서 제공됩니다.</Note>
모든 암호화된 값은 `enc:v2:\<keyId>:\<payload>` 형식으로 래핑됩니다. 여기서 `\<keyId>`는 원시 키에서 파생된 8자리 16진수 접두사입니다. 회전 작업은 온라인으로 수행되며, 중단 후 재개할 수 있습니다.
1. **새 키 생성**: `openssl rand -base64 32`.
2. `.env`에 **두 키를 나란히 설정**한 다음, 재시작합니다:
```ini
ENCRYPTION_KEY=NEW_VALUE
FALLBACK_ENCRYPTION_KEY=OLD_VALUE
```
새로 기록되는 데이터는 새 키를 사용하며, 기존 행은 폴백을 통해 계속 복호화됩니다.
3. **기존 행 재암호화**:
```bash
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
```
이 커맨드는 여섯 개의 위치(`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`)를 순회합니다. SQL 필터는 이미 새 `\<keyId>`로 암호화된 행을 건너뛰므로, 이 커맨드는 멱등성을 가집니다. 필요에 따라 중단하고 다시 실행해도 됩니다. 어떤 행이라도 실패하면 0이 아닌 코드로 종료되며, 재실행하여 재시도할 수 있습니다.
| 플래그 | 설명 |
| ---------------------------------------- | ---------------------------------------- |
| `-s, --site \<site>` | 단일 사이트로 범위를 제한합니다. |
| `-b, --batch-size \<n>` | 배치당 행 수(기본값 `200`, 최대 `5000`). |
| `-d, --dry-run` | 메모리에서 복호화 + 재암호화를 수행하고, `UPDATE`는 생략합니다. |
4. `--dry-run` 결과 남은 행이 0이면 **폴백을 제거**합니다. `FALLBACK_ENCRYPTION_KEY`를 삭제하고 재시작합니다.
## 레거시 `APP_SECRET` 지원
`ENCRYPTION_KEY`를 설정하지 않은 오래된 인스턴스는 정지 상태 암호화 키(및 이로부터 파생된 세션 쿠키 시크릿)로 `APP_SECRET`을 사용합니다. 이 경로는 하위 호환성을 위해 유지되지만 **사용 중단 예정**입니다. 전용 `ENCRYPTION_KEY`를 설정하고 위의 회전 절차를 따라 이를 사용하지 않는 방식으로 마이그레이션하세요. `APP_SECRET` 자체는 기존 HS256 액세스 토큰을 검증하는 데 계속 사용됩니다.
@@ -1,11 +1,12 @@
---
title: 설정
icon: 톱니바퀴
---
# 구성 관리
<Warning>
**처음 설치하시나요?** [Docker Compose 설치 가이드](/l/ko/developers/self-host/capabilities/docker-compose)를 따라 Twenty를 실행하고, 이곳으로 돌아와 구성을 진행하세요.
**처음 설치하시나요?** [Docker Compose 설치 가이드](/l/ko/developers/self-host/capabilities/docker-compose)를 따라 Twenty를 실행하고, 이곳으로 돌아와 구성을 진행하세요.
</Warning>
Twenty는 다른 배포 요구에 맞추기 위해 **두 가지 구성 모드**를 제공합니다:
@@ -26,7 +27,7 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
4. 변경 사항은 즉시 적용됩니다 (멀티 컨테이너 배포의 경우 15초 이내)
<Warning>
**멀티 컨테이너 배포:** 데이터베이스 구성을 사용할 때 (`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`), 서버와 작업자 컨테이너 모두 동일한 데이터베이스에서 읽습니다. 관리자 패널의 변경은 둘 다 자동으로 영향을 미쳐 환경 변수를 컨테이너 간에 복제할 필요가 없습니다 (인프라 변수 제외).
**멀티 컨테이너 배포:** 데이터베이스 구성을 사용할 때 (`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`), 서버와 작업자 컨테이너 모두 동일한 데이터베이스에서 읽습니다. 관리자 패널의 변경은 둘 다 자동으로 영향을 미쳐 환경 변수를 컨테이너 간에 복제할 필요가 없습니다 (인프라 변수 제외).
</Warning>
**관리자 패널에서 구성할 수 있는 내용:**
@@ -41,12 +42,27 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
![관리자 패널 구성 변수](/images/user-guide/setup/admin-panel-config-variables.png)
<Warning>
각 변수는 **설정 → 관리자 패널 → 구성 변수**의 관리자 패널에 설명과 함께 문서화되어 있습니다.
데이터베이스 연결 (`PG_DATABASE_URL`), 서버 URL (`SERVER_URL`), 비밀 (`APP_SECRET`)과 같은 일부 인프라 설정은 `.env` 파일을 통해서만 구성할 수 있습니다.
각 변수는 **설정 → 관리자 패널 → 구성 변수**의 관리자 패널에 설명과 함께 문서화되어 있습니다.
데이터베이스 연결(`PG_DATABASE_URL`), 서버 URL(`SERVER_URL`), 비밀 (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`)과 같은 일부 인프라 설정은 `.env` 파일을 통해서만 구성할 수 있습니다.
[완전한 기술적 참조 →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
[완전한 기술적 참조 →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
</Warning>
## 암호화 키
Twenty는 환경 변수로만 설정할 수 있는 암호화 키를 두 개 사용합니다:
| 변수 | 목적 | 필수 |
| ------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `ENCRYPTION_KEY` | 휴지 상태의 비밀(OAuth 토큰, 애플리케이션 변수, 서명 키 개인 키, TOTP 비밀, 민감한 구성 값)을 암호화하는 데 사용되는 기본 키입니다. | 새로운 설치의 경우 예(레거시 설치는 대신 `APP_SECRET`에 의존할 수 있습니다 — 아래 참조) |
| `FALLBACK_ENCRYPTION_KEY` | 검증 전용 키입니다. 회전 중 기존 행을 계속 복호화할 수 있도록, *이전* `ENCRYPTION_KEY`로 설정합니다. | 회전 중에만 |
하위 호환을 위해 `ENCRYPTION_KEY`가 설정되지 않은 경우, Twenty는 휴지 상태 암호화에 대해 `APP_SECRET`을 대신 사용하여 기존 배포의 레거시 동작과 일치합니다. 새로운 설치에서는 항상 전용 `ENCRYPTION_KEY`를 설정해야 합니다.
`openssl rand -base64 32`로 값을 생성한 뒤 비밀 관리자, 봉인된 구성 등 안전한 위치에 저장하세요. `ENCRYPTION_KEY`를 분실하면 데이터베이스에 저장된 모든 비밀에 대한 접근을 잃게 됩니다.
중단 없이 `ENCRYPTION_KEY`를 회전하는 방법은 [키 회전 가이드](/l/ko/developers/self-host/capabilities/key-rotation)를 참고하세요.
## 2. 환경 전용 구성
```bash
@@ -93,7 +109,7 @@ DEFAULT_SUBDOMAIN=app # default value
* 서브도메인 및 사용자 지정 도메인과 같은 워크스페이스 전용 설정이 워크스페이스 설정에서 제공됩니다
<Warning>
**환경 전용 설정:** `IS_MULTIWORKSPACE_ENABLED`는 `.env` 파일을 통해서만 구성할 수 있으며 재시작이 필요합니다. 관리자 패널을 통해 변경할 수 없습니다.
**환경 전용 설정:** `IS_MULTIWORKSPACE_ENABLED`는 `.env` 파일을 통해서만 구성할 수 있으며 재시작이 필요합니다. 관리자 패널을 통해 변경할 수 없습니다.
</Warning>
### 멀티 워크스페이스를 위한 DNS 구성
@@ -149,7 +165,7 @@ IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=true
* `AUTH_GOOGLE_APIS_CALLBACK_URL=https://{your-domain}/auth/google-apis/get-access-token`
<Warning>
**환경 전용 모드:** `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`로 설정한 경우, `.env` 파일에 이러한 변수를 추가하세요.
**환경 전용 모드:** `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`로 설정한 경우, `.env` 파일에 이러한 변수를 추가하세요.
</Warning>
**필요한 범위** (자동 구성됨):
@@ -168,7 +184,7 @@ IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=true
## Microsoft 365 통합
<Warning>
사용자는 캘린더 및 메시징 API를 사용하려면 [Microsoft 365 라이선스](https://admin.microsoft.com/Adminportal/Home)를 보유해야 합니다. 없이는 Twenty에서 계정을 동기화할 수 없습니다.
사용자는 캘린더 및 메시징 API를 사용하려면 [Microsoft 365 라이선스](https://admin.microsoft.com/Adminportal/Home)를 보유해야 합니다. 없이는 Twenty에서 계정을 동기화할 수 없습니다.
</Warning>
### Microsoft Azure에 프로젝트 생성
@@ -211,7 +227,7 @@ Microsoft Azure 콘솔에서 "권한"에서 다음 API를 활성화하세요:
* `AUTH_MICROSOFT_APIS_CALLBACK_URL=https://{your-domain}/auth/microsoft-apis/get-access-token`
<Warning>
**환경 전용 모드:** `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`로 설정한 경우, `.env` 파일에 이러한 변수를 추가하세요.
**환경 전용 모드:** `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`로 설정한 경우, `.env` 파일에 이러한 변수를 추가하세요.
</Warning>
### 범위 구성
@@ -256,82 +272,111 @@ yarn command:prod cron:workflow:automated-cron-trigger
3. SMTP 설정을 구성하세요:
<ArticleTabs label1="Gmail" label2="오피스365" label3="Smtp4dev">
<ArticleTab>
[App Password](https://support.google.com/accounts/answer/185833)를 프로비저닝해야 합니다.
<ArticleTab>
[App Password](https://support.google.com/accounts/answer/185833)를 프로비저닝해야 합니다.
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=smtp.gmail.com
* EMAIL_SMTP_PORT=465
* EMAIL_SMTP_USER=gmail_email_address
* EMAIL_SMTP_PASSWORD='gmail_app_password'
</ArticleTab>
<ArticleTab>
2단계 인증을 사용 중인 경우, [앱 비밀번호](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9)를 발급받아야 합니다.
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=smtp.office365.com
* EMAIL_SMTP_PORT=587
* EMAIL_SMTP_USER=office365_email_address
* EMAIL_SMTP_PASSWORD='office365_password'
</ArticleTab>
<ArticleTab>
**smtp4dev**는 개발 및 테스트를 위한 가상의 SMTP 이메일 서버입니다.
* smtp4dev 이미지를 실행하세요: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
* smtp4dev UI에 여기에 접근하세요: [http://localhost:8090](http://localhost:8090)
* 다음 변수를 설정하세요:
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=smtp.gmail.com
* EMAIL_SMTP_PORT=465
* EMAIL_SMTP_USER=gmail_email_address
* EMAIL_SMTP_PASSWORD='gmail_app_password'
* EMAIL_SMTP_HOST=localhost
* EMAIL_SMTP_PORT=2525
</ArticleTab>
<ArticleTab>
2단계 인증을 사용 중인 경우, [앱 비밀번호](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9)를 발급받아야 합니다.
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=smtp.office365.com
* EMAIL_SMTP_PORT=587
* EMAIL_SMTP_USER=office365_email_address
* EMAIL_SMTP_PASSWORD='office365_password'
</ArticleTab>
<ArticleTab>
**smtp4dev**는 개발 및 테스트를 위한 가상의 SMTP 이메일 서버입니다.
* smtp4dev 이미지를 실행하세요: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
* smtp4dev UI에 여기에 접근하세요: [http://localhost:8090](http://localhost:8090)
* 다음 변수를 설정하세요:
* EMAIL_DRIVER=smtp
* EMAIL_SMTP_HOST=localhost
* EMAIL_SMTP_PORT=2525
</ArticleTab>
</ArticleTabs>
<Warning>
**환경 전용 모드:** `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`로 설정한 경우, `.env` 파일에 이러한 변수를 추가하세요.
**환경 전용 모드:** `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`로 설정한 경우, `.env` 파일에 이러한 변수를 추가하세요.
</Warning>
## 로직 함수
Twenty는 워크플로 및 사용자 지정 로직을 위해 로직 함수를 지원합니다. `SERVERLESS_TYPE` 환경 변수를 통해 실행 환경을 구성합니다.
## S3 저장소
<Warning>
**보안 공지:** 로컬 드라이버 (`SERVERLESS_TYPE=LOCAL`)는 샌드박싱 없이 호스트의 Node.js 프로세스에서 코드를 직접 실행합니다. 개발 환경에서 신뢰할 수 있는 코드에만 사용해야 합니다. 신뢰할 수 없는 코드를 처리하는 프로덕션 배포의 경우 `SERVERLESS_TYPE=LAMBDA` 또는 `SERVERLESS_TYPE=DISABLED` 사용을 강력히 권장합니다.
기본적으로 Twenty는 업로드된 파일을 로컬 파일 시스템에 저장합니다. 프로덕션 배포에서는 S3 또는 S3 호환 서비스(MinIO, DigitalOcean Spaces 등)를 사용하세요. 컨테이너 재시작 시에도 파일이 유지되고, 여러 서버 인스턴스에 걸쳐 확장될 수 있도록 하기 위함입니다.
</Warning>
### 사용 가능한 드라이버
`STORAGE_TYPE=S_3`를 설정하고 관리자 패널 또는 `.env`를 통해 `STORAGE_S3_*` 변수를 구성하세요. 전체 S3 변수 목록은 [config-variables.ts 참조](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)를 확인하세요.
| 드라이버 | 환경 변수 | 사용 사례 | 보안 수준 |
| ------ | -------------------------- | ------------------------- | --------------- |
| 비활성화 | `SERVERLESS_TYPE=DISABLED` | 로직 함수를 완전히 비활성화 | 해당 없음 |
| 로컬 | `SERVERLESS_TYPE=LOCAL` | 개발 및 신뢰할 수 있는 환경 | 낮음 (샌드박싱 없음) |
| Lambda | `SERVERLESS_TYPE=LAMBDA` | 신뢰할 수 없는 코드를 처리하는 프로덕션 환경 | 높음 (하드웨어 수준 격리) |
CORS에 의존하는 기능(예: 브라우저 내 파일 다운로드)과 함께 S3를 사용할 때에는, 버킷의 CORS 구성에서 Twenty 프런트엔드 오리진을 허용하도록 하세요.
### 권장 구성
## 로직 함수 및 코드 인터프리터
Twenty는 워크플로를 위한 로직 함수와 AI 데이터 분석을 위한 코드 인터프리터를 지원합니다. 두 기능 모두 사용자 제공 코드를 실행하며, 보안을 위해 명시적인 구성이 필요합니다.
### 보안 기본값
**프로덕션 (NODE_ENV=production):** 로직 함수와 코드 인터프리터는 기본값이 **비활성화됨**입니다. 이러한 기능이 필요하다면 `LOGIC_FUNCTION_TYPE` 및 `CODE_INTERPRETER_TYPE`로 명시적으로 활성화해야 합니다.
**개발 (NODE_ENV=development):** 로컬에서 실행할 때 편의를 위해 둘 다 기본값이 **LOCAL**입니다.
<Warning>
**보안 공지:** 로컬 드라이버 (`LOGIC_FUNCTION_TYPE=LOCAL` 또는 `CODE_INTERPRETER_TYPE=LOCAL`)는 샌드박싱 없이 호스트의 Node.js 프로세스에서 코드를 직접 실행합니다. 개발 환경에서 신뢰할 수 있는 코드에만 사용해야 합니다. 신뢰할 수 없는 코드를 처리하는 프로덕션 배포에서는 `LOGIC_FUNCTION_TYPE=LAMBDA` 또는 `CODE_INTERPRETER_TYPE=E2B`(샌드박싱 적용)을 사용하거나 비활성화 상태로 유지하세요.
</Warning>
### 로직 함수 - 사용 가능한 드라이버
| 드라이버 | 환경 변수 | 사용 사례 | 보안 수준 |
| ------ | ------------------------------ | ------------------------- | --------------- |
| 비활성화 | `LOGIC_FUNCTION_TYPE=DISABLED` | 로직 함수를 완전히 비활성화 | 해당 없음 |
| 로컬 | `LOGIC_FUNCTION_TYPE=LOCAL` | 개발 및 신뢰할 수 있는 환경 | 낮음 (샌드박싱 없음) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | 신뢰할 수 없는 코드를 처리하는 프로덕션 환경 | 높음 (하드웨어 수준 격리) |
### 로직 함수 - 권장 구성
**개발:**
```bash
SERVERLESS_TYPE=LOCAL # default
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**프로덕션(AWS):**
```bash
SERVERLESS_TYPE=LAMBDA
SERVERLESS_LAMBDA_REGION=us-east-1
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**로직 함수를 비활성화하려면:**
```bash
SERVERLESS_TYPE=DISABLED
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### 코드 인터프리터 - 사용 가능한 드라이버
| 드라이버 | 환경 변수 | 사용 사례 | 보안 수준 |
| ---- | -------------------------------- | ------------------ | ------------- |
| 비활성화 | `CODE_INTERPRETER_TYPE=DISABLED` | AI 코드 실행 비활성화 | 해당 없음 |
| 로컬 | `CODE_INTERPRETER_TYPE=LOCAL` | 개발 전용 | 낮음 (샌드박싱 없음) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | 샌드박스 실행을 사용하는 프로덕션 | 높음 (격리된 샌드박스) |
<Note>
`SERVERLESS_TYPE=DISABLED`를 사용하는 경우 로직 함수를 실행하려는 모든 시도는 오류를 반환합니다. 로직 함수 기능 없이 Twenty를 실행하려는 경우 유용합니다.
`LOGIC_FUNCTION_TYPE=DISABLED` 또는 `CODE_INTERPRETER_TYPE=DISABLED`를 사용하는 경우, 모든 실행 시도는 오류를 반환합니다. 이러한 기능 없이 Twenty를 실행하려는 경우 유용합니다.
</Note>
@@ -1,5 +1,6 @@
---
title: 문제 해결
icon: wrench
---
## 문제 해결
@@ -53,7 +54,7 @@ Twenty 설치 중에 적절한 스키마, 확장, 사용자를 사용하여 post
#### 저장 시 린트가 작동하지 않음
Oxc 확장 (`oxc.oxc-vscode`) 프로그램이 설치된 상태에서는 기본 설정으로 작동해야 합니다. 작동하지 않으면 vscode 설정(개발 컨테이너 범위)에 다음을 추가해 보세요:
This should work out of the box with the Oxc extension (`oxc.oxc-vscode`) installed. 작동하지 않으면 vscode 설정(개발 컨테이너 범위)에 다음을 추가해 보세요:
```
"editor.codeActionsOnSave": {
@@ -166,6 +167,10 @@ plugins: [
데이터베이스 컨테이너에서 관리자 패널에 액세스하기 위해 `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';`을 실행하십시오.
#### 워크플로우를 실행하면 다음 오류로 실행이 실패합니다: "로직 함수 실행이 비활성화되었습니다. 활성화하려면 LOGIC_FUNCTION_TYPE을 LOCAL 또는 LAMBDA로 설정하세요."
프로덕션 환경에서는 로직 함수가 기본적으로 비활성화되어 있습니다. 이를 활성화하려면 `LOGIC_FUNCTION_TYPE` 환경 변수를 `LOCAL` 또는 `LAMBDA`로 설정하세요. 이는 환경 변수를 통해서나 관리자 패널의 데이터베이스 변수를 통해 구성할 수 있습니다. 자세한 내용은 [로직 함수 설정 가이드](/l/ko/developers/self-host/capabilities/setup#logic-functions-available-drivers)를 참조하세요.
### 1-클릭 Docker 구성
#### 로그인할 수 없음
@@ -1,372 +1,102 @@
---
title: 업그레이드 가이드
icon: arrow-up-right-dots
---
## 일반 가이드라인
**업그레이드 프로세스를 시작하기 전에 다음 명령을 실행하여 데이터베이스를 반드시 백업하십시오**: `docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql`.
백업을 복원하려면 `cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}`를 실행하십시오.
Docker Compose를 사용한 경우, 다음 단계를 따르십시오:
1. Twenty가 실행되고 있는 호스트의 터미널에서 Twenty를 종료하십시오: `docker compose down`
2. `TAG` 값을 docker-compose와 가까운 .env 파일에 변경하여 버전을 업그레이드하십시오. ( `v0.53`과 같은 `major.minor` 버전 사용을 권장합니다 )
3. `docker compose up -d`로 Twenty를 다시 온라인 상태로 전환하십시오.
여러 버전으로 인스턴스를 업그레이드하려는 경우, 예를 들어 v0.33.0에서 v0.35.0으로 업그레이드하려면 v0.33.0에서 v0.34.0으로, 그런 다음 v0.34.0에서 v0.35.0으로 업그레이드하십시오.
**각 버전 업그레이드 후마다 백업이 손상되지 않았는지 확인하십시오.**
## 버전별 업그레이드 단계
## v1.0
안녕하세요 Twenty v1.0! 🎉
## v0.60
### 성능 개선
메타데이터 API와의 모든 상호작용이 최적화되어, 특히 객체 메타데이터 조작 및 워크스페이스 생성 작업에서 성능이 향상되었습니다.
데이터베이스 쿼리보다는 캐시 히트를 우선시하도록 캐싱 전략을 재구성하여 메타데이터 API 작업의 성능을 대폭 향상시켰습니다.
업그레이드 후 실행 중 문제가 발생하면 캐시를 플러시하여 최신 변경 사항과 동기화해야 할 수도 있습니다. twenty-server 컨테이너에서 이 명령을 실행하십시오:
**업그레이드 프로세스를 시작하기 전에 항상 데이터베이스를 백업하십시오** 다음 명령을 실행하세요:
```bash
yarn command:prod cache:flush
docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql
```
### v0.55
Twenty 인스턴스를 v0.55 이미지로 업그레이드하십시오.
이제 명령어를 실행할 필요가 없으며, 새로운 이미지는 모든 필요한 마이그레이션을 자동으로 처리할 것입니다.
### `사용자에게 권한이 없습니다` 오류
업그레이드 후 대부분의 요청에서 권한 오류가 발생하면 캐시를 플러시하여 최신 권한을 다시 계산해야 할 수도 있습니다.
`twenty-server` 컨테이너에서 실행하십시오:
백업에서 복원하려면:
```bash
yarn command:prod cache:flush
cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}
```
이 문제는 이 Twenty 버전에만 해당하며, 향후 업그레이드에서는 필요하지 않을 것입니다.
Docker Compose를 사용하는 경우 다음 단계를 따르십시오:
### v0.54
1. Twenty 중지: `docker compose down`
2. `docker-compose.yml` 옆에 있는 `.env` 파일에서 `TAG` 값을 변경하십시오
3. Twenty 시작: `docker compose up -d`
버전 `0.53` 이후로 수동 작업이 필요하지 않습니다.
서버는 시작 시 필요한 모든 업그레이드 마이그레이션을 자동으로 실행합니다. 수동 명령은 필요하지 않습니다.
#### 메타데이터 스키마 사용 중단
## 교차 버전 업그레이드(v1.22+)
`metadata` 스키마를 `core` 스키마로 병합하여 `TypeORM`에서 데이터 검색을 간소화했습니다.
`migrate` 명령 단계를 `upgrade` 명령과 통합했습니다. 서버/작업자 컨테이너 내에서 `migrate`를 수동으로 실행하지 않는 것을 권장합니다.
**v1.22**부터 Twenty는 교차 버전 업그레이드를 지원합니다. 지원되는 어떤 버전에서든 각 중간 버전을 거치지 않고 최신 릴리스로 바로 이동할 수 있습니다.
### Since v0.53
예를 들어, v1.22에서 v2.0으로 바로 업그레이드하는 것이 완전히 지원됩니다.
`0.53`부터 업그레이드는 `DockerFile` 내에서 프로그래밍 방식으로 수행되므로 앞으로는 수동으로 명령을 실행할 필요가 없습니다.
## v2.5+로 업그레이드 — 저장 상태 암호화 엔벨로프
인스턴스를 순차적으로 업그레이드하고 주요 버전을 건너뛰지 마십시오(e.g. `0.43.3`에서 `0.44.0`으로의 업그레이드는 가능하지만 `0.43.1`에서 `0.45.0`으로의 업그레이드는 불가능), 그렇지 않으면 워크스페이스 버전 비동기화가 발생하여 런타임 오류 및 기능 누락이 발생할 수 있습니다.
**v2.5**부터 Twenty는 저장 상태의 시크릿(OAuth 토큰, 애플리케이션 변수, 서명 키의 개인 키, 민감한 설정값, TOTP 시크릿)을 버전이 지정된 `enc:v2:` 엔벨로프에 담아, `ENCRYPTION_KEY`(또는 `ENCRYPTION_KEY`가 설정되지 않은 경우 `APP_SECRET`)로 암호화해 저장합니다.
워크스페이스가 올바르게 마이그레이션되었는지 확인하려면 `core.workspace` 테이블에서 데이터베이스의 버전을 검토할 수 있습니다.
v2.5에서 첫 부팅 시, 기존 행을 새로운 엔벨로프에 백필하는 속도가 느린 업그레이드 명령이 실행됩니다. 이는 멱등적이어서 — 서버를 중단했다가 다시 시작해도 중단된 지점부터 재개되지만 — 대규모 데이터베이스에서는 시간이 오래 걸릴 수 있습니다. `upgrade:status`로 진행 상황을 모니터링할 수 있습니다.
항상 현재 Twenty 인스턴스의 `주.소` 버전 범위 내에 있어야 하며, 인스턴스 버전은 admin 패널에서 볼 수 있습니다(`/설정/admin-panel`, 사용자가 데이터베이스에서 `canAccessFullAdminPanel` 속성이 true로 설정된 경우 접근 가능) 또는 `twenty-server` 컨테이너에서 `echo $APP_VERSION`을 실행하여 확인할 수 있습니다.
백필이 처음부터 해당 키로 행을 기록할 수 있도록, v2.5 업그레이드 **이전에** 전용 `ENCRYPTION_KEY`를 설정해야 합니다. 백필 이후에 키를 변경하려면 [키 회전](/l/ko/developers/self-host/capabilities/key-rotation)이 필요합니다.
비동기화된 워크스페이스 버전을 수정하려면 해당 Twenty 버전의 관련 업그레이드 가이드를 따르고 원하는 버전에 도달할 때까지 순차적으로 업그레이드해야 합니다.
## 시크릿 및 서명 키 회전
#### `auditLog` 제거
`ENCRYPTION_KEY` 회전, JWT 서명 키 회전, 유출된 서명 키 폐기와 같은 일상 운영 작업에 대해서는 전용 [키 회전 가이드](/l/ko/developers/self-host/capabilities/key-rotation)를 참조하세요.
auditLog 표준 객체를 제거했으며, 이로 인해 이 마이그레이션 이후 백업 크기가 상당히 줄어들 수 있습니다.
## 업그레이드 상태 확인
### v0.51 to v0.52
`upgrade:status` 명령을 사용하면 인스턴스 및 워크스페이스 마이그레이션의 현재 상태를 확인할 수 있습니다. 업그레이드 문제를 디버깅하거나 지원 요청을 제출할 때 유용합니다.
Twenty 인스턴스를 v0.52 이미지로 업그레이드하십시오.
서버 컨테이너에서 실행하십시오:
```
yarn database:migrate:prod
yarn command:prod upgrade
```bash
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status
```
#### 버전이 `0.52.0`과 `0.52.6` 사이에 차단된 워크스페이스가 있습니다.
출력 예:
불행히도 `0.52.0` 및 `0.52.6`은 dockerHub에서 완전히 삭제되었습니다.
데이터베이스의 워크스페이스 버전을 `0.51.0`으로 수동 업데이트하고 바로 위 업그레이드 가이드를 따라 Twenty 버전 `0.52.11`로 업그레이드해야 합니다.
```sh
APP_VERSION: v1.23.0
### v0.50 to v0.51
Instance
Inferred version: 1.23.0
Latest command: 1.23.0_DropWorkspaceVersionColumnFastInstanceCommand_1785000000000
Status: Up to date
Executed by: v1.23.0
At: 2026-04-16T11:43:58.823Z
Twenty 인스턴스를 v0.51 이미지로 업그레이드하십시오.
Workspace
Apple (20202020-1c25-4d02-bf25-6aeccf7ea419)
Inferred version: 1.23.0
Latest command: 1.23.0_UpdateGlobalObjectContextCommandMenuItemsCommand_1780000005000
Status: Up to date
Executed by: v1.23.0
At: 2026-04-16T11:44:09.361Z
```
yarn database:migrate:prod
yarn command:prod upgrade
Summary
Instance: Up to date
Workspaces: 1 up to date, 0 behind, 0 failed (1 total)
```
### v0.44.0 to v0.50.0
### 옵션
Twenty 인스턴스를 v0.50.0 이미지로 업그레이드하십시오.
| 플래그 | 설명 |
| ------------------------- | ---------------------------------------- |
| `-w, --workspace-id <id>` | 특정 워크스페이스로 필터링합니다. 여러 번 지정할 수 있습니다. |
| `-f, --failed-only` | 최신 상태의 워크스페이스는 숨기고, 뒤처졌거나 실패한 항목만 표시합니다. |
```
yarn database:migrate:prod
yarn command:prod upgrade
## 문제 해결
일부 워크스페이스에서 업그레이드가 실패하면 서버는 실패한 단계 이후로 진행하지 않습니다. 서버를 다시 시작하면(`docker compose up -d`) 중단된 지점부터 업그레이드를 다시 시도합니다.
문제를 빠르게 식별하려면 다음을 실행하십시오:
```bash
docker exec -it {server_container_name_or_id} yarn command:prod upgrade:status --failed-only
```
#### Docker-compose.yml 변경
이는 뒤처졌거나 실패한 워크스페이스만 표시하고 각 실패에 대한 오류 메시지도 함께 표시합니다.
이 버전은 `worker` 서비스가 `server-local-data` 볼륨에 접근할 수 있도록 `docker-compose.yml` 변경을 포함합니다.
로컬 `docker-compose.yml`을 [v0.50.0 docker-compose.yml](https://github.com/twentyhq/twenty/blob/v0.50.0/packages/twenty-docker/docker-compose.yml)로 업데이트하십시오.
## v1.22 이전
### v0.43.0 to v0.44.0
Twenty 인스턴스를 v0.44.0 이미지로 업그레이드하십시오.
```
yarn database:migrate:prod
yarn command:prod upgrade
```
### v0.42.0 to v0.43.0
Twenty 인스턴스를 v0.43.0 이미지로 업그레이드하십시오.
```
yarn database:migrate:prod
yarn command:prod upgrade
```
이 버전에서는 docker-compose.yml 이미지가 postgres:16으로 전환되었습니다.
#### (옵션 1) 데이터베이스 마이그레이션
기존 postgres-spilo 이미지를 유지해도 되지만, docker-compose.yml에서 버전을 0.43.0으로 고정해야 합니다.
#### (옵션 2) 데이터베이스 마이그레이션
데이터베이스를 새로운 postgres:16 이미지로 마이그레이션하려면, 다음 단계를 따르십시오:
1. 이전 postgres-spilo 컨테이너에서 데이터베이스를 덤프하십시오.
```
docker exec -it twenty-db-1 sh
pg_dump -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} > databases_backup.sql
exit
docker cp twenty-db-1:/home/postgres/databases_backup.sql .
```
덤프 파일이 비어 있지 않은지 확인하십시오.
2. docker-compose.yml을 postgres:16 이미지를 사용하도록 [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) 파일에 따라 업그레이드하십시오.
3. 새로운 postgres:16 컨테이너에 데이터베이스를 복원하십시오.
```
docker cp databases_backup.sql twenty-db-1:/databases_backup.sql
docker exec -it twenty-db-1 sh
psql -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} -f databases_backup.sql
exit
```
### v0.41.0 to v0.42.0
Twenty 인스턴스를 v0.42.0 이미지로 업그레이드하십시오.
```
yarn database:migrate:prod
yarn command:prod upgrade-0.42
```
**환경 변수**
* 삭제됨: `FRONT_PORT`, `FRONT_PROTOCOL`, `FRONT_DOMAIN`, `PORT`
* 추가됨: `FRONTEND_URL`, `NODE_PORT`, `MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION`, `MESSAGING_PROVIDER_MICROSOFT_ENABLED`, `CALENDAR_PROVIDER_MICROSOFT_ENABLED`, `IS_MICROSOFT_SYNC_ENABLED`
### v0.40.0 to v0.41.0
Twenty 인스턴스를 v0.41.0 이미지로 업그레이드하십시오.
```
yarn database:migrate:prod
yarn command:prod upgrade-0.41
```
**환경 변수**
* 삭제됨: `AUTH_MICROSOFT_TENANT_ID`
### v0.35.0 to v0.40.0
Twenty 인스턴스를 v0.40.0 이미지로 업그레이드하십시오.
```
yarn database:migrate:prod
yarn command:prod upgrade-0.40
```
**환경 변수**
* 추가됨: `IS_EMAIL_VERIFICATION_REQUIRED`, `EMAIL_VERIFICATION_TOKEN_EXPIRES_IN`, `WORKFLOW_EXEC_THROTTLE_LIMIT`, `WORKFLOW_EXEC_THROTTLE_TTL`
### v0.34.0 to v0.35.0
Twenty 인스턴스를 v0.35.0 이미지로 업그레이드하십시오.
```
yarn database:migrate:prod
yarn command:prod upgrade-0.35
```
`yarn database:migrate:prod` 명령은 데이터베이스 구조(핵심 및 메타데이터 스키마)에 마이그레이션을 적용합니다. `yarn command:prod upgrade-0.35`는 모든 워크스페이스의 데이터 마이그레이션을 처리합니다.
**환경 변수**
* `ENABLE_DB_MIGRATIONS`를 `DISABLE_DB_MIGRATIONS`로 교체했습니다(기본값은 이제 `false`이며, 별도의 설정이 필요하지 않을 것입니다.)
### v0.33.0 to v0.34.0
Twenty 인스턴스를 v0.34.0 이미지로 업그레이드하십시오.
```
yarn database:migrate:prod
yarn command:prod upgrade-0.34
```
`yarn database:migrate:prod` 명령은 데이터베이스 구조(핵심 및 메타데이터 스키마)에 마이그레이션을 적용합니다. `yarn command:prod upgrade-0.34`는 모든 워크스페이스의 데이터 마이그레이션을 처리합니다.
**환경 변수**
* 삭제됨: `FRONT_BASE_URL`
* 추가됨: `FRONT_DOMAIN`, `FRONT_PROTOCOL`, `FRONT_PORT`
프런트엔드 URL을 처리하는 방식을 업데이트했습니다.
이제 `FRONT_DOMAIN`, `FRONT_PROTOCOL` 및 `FRONT_PORT` 변수를 사용하여 프런트엔드 URL을 설정할 수 있습니다.
`FRONT_DOMAIN`이 설정되지 않은 경우 프런트엔드 URL은 `SERVER_URL`로 대체됩니다.
### v0.32.0 to v0.33.0
Twenty 인스턴스를 v0.33.0 이미지로 업그레이드하십시오.
```
yarn command:prod cache:flush
yarn database:migrate:prod
yarn command:prod upgrade-0.33
```
`yarn command:prod cache:flush` 명령은 Redis 캐시를 플러시합니다.
`yarn database:migrate:prod` 명령은 데이터베이스 구조(핵심 및 메타데이터 스키마)에 마이그레이션을 적용합니다. `yarn command:prod upgrade-0.33`는 모든 워크스페이스의 데이터 마이그레이션을 처리합니다.
이 버전부터 DB용으로 twenty-postgres 이미지가 폐기되어 twenty-postgres-spilo가 대신 사용되었습니다.
twenty-postgres 이미지를 계속 사용하려면, docker-compose.yml에서 `twentycrm/twenty-postgres:${TAG}`를 `twentycrm/twenty-postgres`로 교체하십시오.
### v0.31.0 to v0.32.0
Twenty 인스턴스를 v0.32.0 이미지로 업그레이드하십시오.
**스키마 및 데이터 마이그레이션**
```
yarn database:migrate:prod
yarn command:prod upgrade-0.32
```
`yarn database:migrate:prod` 명령은 데이터베이스 구조(핵심 및 메타데이터 스키마)에 마이그레이션을 적용합니다. `yarn command:prod upgrade-0.32`는 모든 워크스페이스의 데이터 마이그레이션을 처리합니다.
**환경 변수**
Redis 연결을 처리하는 방식을 업데이트했습니다.
* 삭제됨: `REDIS_HOST`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`
* 추가됨: `REDIS_URL`
`.env` 파일을 업데이트하여 개별 Redis 연결 매개변수 대신 새 `REDIS_URL` 변수를 사용하십시오.
JWT 토큰을 처리하는 방식도 간소화했습니다.
* 삭제됨: `ACCESS_TOKEN_SECRET`, `LOGIN_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `FILE_TOKEN_SECRET`
* 추가됨: `APP_SECRET`
.env 파일을 업데이트하여 개별 토큰 비밀 대신 새로운 `APP_SECRET` 변수를 사용하십시오 (이전 비밀을 그대로 사용하거나 새 임의 문자열을 생성할 수 있습니다.)
**연결된 계정**
Google 이메일 및 캘린더 동기화를 위해 연결된 계정을 사용하는 경우, Google 관리 콘솔에서 [People API](https://developers.google.com/people)를 활성화해야 합니다.
### v0.30.0 to v0.31.0
Twenty 인스턴스를 v0.31.0 이미지로 업그레이드하십시오.
**스키마 및 데이터 마이그레이션**:
```
yarn database:migrate:prod
yarn command:prod upgrade-0.31
```
`yarn database:migrate:prod` 명령은 데이터베이스 구조(핵심 및 메타데이터 스키마)에 마이그레이션을 적용합니다. `yarn command:prod upgrade-0.31`는 모든 워크스페이스의 데이터 마이그레이션을 처리합니다.
### v0.24.0 to v0.30.0
Twenty 인스턴스를 v0.30.0 이미지로 업그레이드하십시오.
**중대한 변경 사항**:
성능을 개선하기 위해, 이제 Twenty는 redis 캐시 구성이 필요합니다. [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml)을 업데이트하여 이를 반영했습니다.
구성을 업데이트하고 환경 변수를 적절히 업데이트하십시오:
```
REDIS_HOST={your-redis-host}
REDIS_PORT={your-redis-port}
CACHE_STORAGE_TYPE=redis
```
**스키마 및 데이터 마이그레이션**:
```
yarn database:migrate:prod
yarn command:prod upgrade-0.30
```
`yarn database:migrate:prod` 명령은 데이터베이스 구조(핵심 및 메타데이터 스키마)에 마이그레이션을 적용합니다. `yarn command:prod upgrade-0.30`는 모든 워크스페이스의 데이터 마이그레이션을 처리합니다.
### v0.23.0 to v0.24.0
Twenty 인스턴스를 v0.24.0 이미지로 업그레이드하십시오.
다음 명령어를 실행하십시오:
```
yarn database:migrate:prod
yarn command:prod upgrade-0.24
```
`yarn database:migrate:prod` 명령은 데이터베이스 구조(핵심 및 메타데이터 스키마)에 마이그레이션을 적용합니다. `yarn command:prod upgrade-0.24`는 모든 워크스페이스의 데이터 마이그레이션을 처리합니다.
### v0.22.0 to v0.23.0
Twenty 인스턴스를 v0.23.0 이미지로 업그레이드하십시오.
다음 명령어를 실행하십시오:
```
yarn database:migrate:prod
yarn command:prod upgrade-0.23
```
`yarn database:migrate:prod` 명령은 데이터베이스에 마이그레이션을 적용합니다.
`yarn command:prod upgrade-0.23`는 데이터 마이그레이션을 처리하며, 활동을 작업/노트로 전송하는 것도 포함합니다.
### v0.21.0 to v0.22.0
Twenty 인스턴스를 v0.22.0 이미지로 업그레이드하십시오.
다음 명령어를 실행하십시오:
```
yarn database:migrate:prod
yarn command:prod upgrade-0.22
```
`yarn database:migrate:prod` 명령은 데이터베이스에 마이그레이션을 적용합니다.
`yarn command:prod upgrade-0.22` 명령은 새 객체 defaultRequestInstrumentationOptions에 적응하기 위한 특정 데이터 변환을 적용합니다.
인스턴스가 v1.22보다 오래된 경우, v1.22에 도달할 때까지 각 주요 태그 버전(v1.6에서 v1.7, 이어서 v1.7에서 v1.8 등)을 순차적으로 업그레이드해야 합니다. 그 이후에는 최신 버전으로 바로 업그레이드할 수 있습니다.