사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
업무 동작 Endpoint
모든 모델에는 생성, 수정, 삭제가 이미 있습니다. 화면에 발행, 승인, 거절, 보관, 알림 발송 같은 분명한 업무 동작이 하나 더 필요하면 그때 endpoint를 씁니다.
CRUD — 자동 생성
모든 모델에 딸려 옵니다. 따로 endpoint를 쓰지 않습니다.
createPost · updatePost · removePostEndpoint — 직접 작성
업무 동작 하나를 동사 이름으로 만듭니다.
publishPost · approveTicket · archiveProject기억할 규칙은 하나입니다. 버튼 하나, store action 하나, endpoint 하나, service 메서드 하나.
이 페이지에서 쓰는 말
용어설명
endpoint
클라이언트가 이름으로 부르는 서버 함수입니다.
<model>.signal.ts에 선언합니다.guard
누가 endpoint를 부를 수 있는지 정하는 클래스입니다. 핸들러보다 먼저 실행됩니다.
service
<model>.service.ts의 서버 클래스입니다. 불러오고, 확인하고, 저장합니다.document chain method
document 클래스의 메서드로, 레코드 하나를 검사하고 바꾼 뒤
this를 반환합니다.store action
st.do.x()로 부르는 클라이언트 메서드입니다. fetch를 호출하고 화면을 갱신합니다.Util
도메인 동작 하나를 담은 작은 클라이언트 컴포넌트로,
<Model>.Util.tsx에 둡니다.흐름 한눈에 보기
게시글의 발행 버튼을 클릭부터 데이터베이스까지 따라가 봅니다. 각 계층은 자기 일 하나만 하고 나머지를 아래로 넘깁니다:
파일하는 일
Post.Util.tsx
버튼. 사용자가 누르는 곳이고, store action만 호출합니다.
post.store.ts
Store action. 생성된 fetch 함수를 호출하고, 결과를 상태에 저장하고, 토스트를 띄웁니다.
post.signal.ts
Endpoint. guard를 실행한 뒤 실제 일을 service에 넘깁니다.
post.service.ts
Service. 게시글을 불러오고, 호출한 사람의 것인지 확인하고, 저장합니다.
post.document.ts
Document. 게시글이 발행할 준비가 됐는지 검사하고 상태를 바꿉니다.
- 클라이언트 호출 코드는 쓰지 않습니다.
fetch.publishPost는 signal에 선언한 endpoint에서 자동으로 생깁니다. - 모델 이름은 바깥 계층에서 다시 붙습니다. document와 service는
publish(), signal과 store와 dictionary는publishPost라고 씁니다.
Endpoint 선언하기
Endpoint는 얇게 둡니다. 인자를 받고, guard를 적고, service를 호출하면 끝입니다. 모델의 signal 파일에서 Endpoint 클래스 안에 씁니다:
apps/myapp/lib/post/post.signal.ts
mutation은 데이터를 바꾸고,query는 읽기만 합니다. 둘 다endpoint()콜백에서 받고, 첫 인자는 반환 타입입니다..with(Self)는 로그인한 사용자입니다. 서버가 채워 주므로, 호출한 사용자의 id를 클라이언트에서 받지 않습니다.exec는 인자를 선언 순서대로 받습니다. 클라이언트 인자가 먼저, 그다음.with()값이 옵니다.this.postService를 쓰려면 화살표 함수가 아니라function으로 씁니다.- 세 클래스는 비어 있어도 모두 선언합니다.
PostInternal,PostSlice,PostEndpoint를 함께 두고, slice의rootguard는 항상Admin입니다.
인자
빌더설명
.param(name, Type)
필수 URL 경로 구간입니다. scalar나
enumOf 하나만 받고(model·배열 불가), 선택 인자보다 앞에 둡니다..body(name, Type, options?)
요청 본문(body) 값이며 주로 mutation에서 씁니다.
{ nullable: true }면 선택 인자가 됩니다..search(name, Type)
URL 쿼리 문자열 값입니다. 항상 선택 인자라서
exec가 undefined를 받을 수 있습니다..with(InternalArg, options?)
서버가 채우는 값이라 클라이언트는 보내지 않습니다.
{ nullable: true }가 없으면 null일 때 호출을 거절합니다.자주 쓰는 옵션
mutation()이나 query()의 두 번째 인자가 옵션 객체입니다:guardsGuardCls[]기본값 없음
핸들러보다 먼저 순서대로 실행되며, 모두 통과해야 하는 guard 클래스입니다.
timeoutnumber (ms)기본값 30초 (클라이언트)
30초보다 오래 걸리는 작업에 선언합니다. 시간이 지나면 호출자는
base.error.gatewayTimeout을 받습니다.mcpboolean기본값 true
false면 AI 에이전트 목록에서만 빠집니다. guard와 HTTP 제공은 그대로입니다.nullableboolean기본값 false
null 반환을 허용합니다. 이 옵션이 없으면 exec는 null을 반환할 수 없습니다.- 처음 거절한 guard가 응답합니다. 그 뒤의 guard와 핸들러는 실행되지 않습니다.
- timeout은 호출자에게 답할 뿐, 작업을 멈추지 않습니다. 핸들러는 결과를 기다리는 쪽이 없어도 끝까지 실행됩니다.


직접 만든 endpoint는 모두 자기 guard를 따로 적습니다. slice의 guard map은 자동 생성된 CRUD에만 적용되고
publishPost에는 닿지 않습니다. guards가 없는 endpoint는 HTTP로 누구나 부를 수 있고 MCP 카탈로그에서도 빠집니다. Public guard만 단 mutation도 마찬가지로 빠집니다.규칙은 service와 document에
업무 규칙은 버튼에도 endpoint에도 두지 않습니다. 각 검사를 어디에 둘지는 무엇을 보는 검사인지로 정합니다:
검사 내용
Guard
post.signal.ts
Service
post.service.ts
Document
post.document.ts
누가 부르는가
부를 자격이 있는가
✓
요청 정책입니다.
guards: [Every]는 로그인하지 않은 호출을 거절합니다.이 게시글의 주인인가
✓
guard를 통과했더라도 service가 소유권을 한 번 더 확인합니다.
무엇이 바뀌는가
여러 document에 걸친 규칙
✓
규칙이 읽는 document를 모두 불러온 뒤 저장하고, 그다음 알립니다.
상태 전제 조건
✓
게시글이
published가 되려면 제목과 내용이 있어야 합니다.✓여기에 둡니다여기가 아닙니다
1. Document: 상태 바꾸기
게시글은 제목과 내용이 있을 때만 발행할 수 있습니다. 이 검사는 레코드 자신에게 둡니다:
apps/myapp/lib/post/post.document.ts
- 검사하고, 바꾸고, 반환합니다. 먼저 검사하고
this를 바꾼 뒤return this로 끝냅니다. - 저장은 하지 않습니다. 호출한 쪽이 한 번만 저장하므로 체인 메서드를 이어 붙일 수 있습니다.
2. Service: 불러오고, 확인하고, 저장하기
service는 게시글을 불러오고, 호출한 사람의 것인지 확인한 뒤, 체인을 실행하고 저장합니다:
apps/myapp/lib/post/post.service.ts
getPost는serve(db.post)가 줍니다. 모든 모델 service에는get<Model>(id)로더가 있습니다.- 관문은 하나가 아니라 둘입니다.
Every는 로그인 여부만 보고, 게시글의 주인인지는 service가 확인합니다. - 마지막 줄은 그대로 씁니다.
return await …save()의await를 빼지 않습니다.
3. Dictionary: key 등록하기
post.error.notReady 같은 key는 module dictionary에 [en, ko] 쌍으로 등록해야 생깁니다:apps/myapp/lib/post/post.dictionary.ts
.error()key는new Err("post.error.notReady")로 던집니다..endpoint()라벨은l("post.signal.publishPost")로 읽습니다..arg()에는 인자를 모두 적습니다..translate()key는msg.success("post.publishSuccess")같은 토스트에 씁니다.


거절은
new Err("<module>.error.<key>")로 합니다. throw new Error는 쓰지 않습니다. 그냥 Error에는 dictionary가 번역할 key가 없고, lint도 막습니다.Store에서 호출하기
클라이언트 컴포넌트는
fetch를 직접 부르지 않고, store action이 부릅니다. 직접 만든 endpoint에는 자동 생성되는 action이 없으므로 하나 작성합니다:apps/myapp/lib/post/post.store.ts
- 본문은 세 줄 정도입니다.
await fetch.x(),this.setPost()같은 자동 생성 setter, 그리고 토스트 순서입니다. - 후처리도 여기에 둡니다. endpoint가 성공한 뒤 모달 닫기나 데이터 갱신도 이 action이 함께 처리합니다.
- action은 값을 반환하지 않습니다. 결과는 setter나
this.set({ … })로 상태에 씁니다. 반환한 값은 호출한 쪽에 닿지 않습니다.
Util 하나로 만들기
버튼은
Post.Util.tsx에 둡니다. 그러면 카드, 상세 페이지, 관리자 페이지 어디서든 같은 action을 재사용할 수 있습니다:apps/myapp/lib/post/Post.Util.tsx
- 항상 클라이언트 컴포넌트입니다. 버튼에
onClick이 있고 store를 쓰므로 첫 줄에"use client"를 둡니다. - 이름은 동사만 씁니다.
PublishPostButton이 아니라Publish로 export하고, page에서는<Post.Util.Publish postId={post.id} />로 씁니다. - 모델이 아니라 id를 받습니다. Util prop 타입을
cnst.Post로 두면 lint에 걸립니다.postId: string을 넘깁니다.
팁과 주의할 점
- Endpoint 이름은 동사로 시작합니다.
publishPost,approveTicket,archiveProject처럼 씁니다. - 업무 규칙은 버튼에 두지 않습니다. service나 document에 둡니다.
- 같은 action이 두 번 보이면 버튼을 복사하기 전에 Util 컴포넌트로 만듭니다.



자동 생성된 CRUD 이름은 다시 쓰지 않습니다.
post, lightPost, createPost, updatePost, removePost, viewPost, editPost, mergePost는 이미 있습니다. Endpoint 클래스에 다시 선언하면 lint 에러가 납니다.이어서 읽기