사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
스키마로 폼 만들기
모델 스키마를 설계했다면 폼은 그 위에 얇게 얹는 UI입니다. 데이터는 폼을 감싼 셸이 준비하고, Template은 필드만 그립니다.
이 페이지에서 쓰는 말
용어설명
Template
<Model>.Template.tsx에 있는 클라이언트 컴포넌트로, 폼의 필드를 그립니다.articleForm
store가 들고 있는, 작성 중인 레코드입니다.
st.use.articleForm()으로 읽습니다.편집 셸
Load.Edit, Model.Edit처럼 폼을 채우고, 열고, 저장하는 래퍼입니다.fetch.slice.<name>
셸에게 어떤 모델을 저장하고 새 레코드를 어느 목록에 넣을지 알려 줍니다.
셸은 폼이 열리는 곳을 보고 고릅니다:
셸설명
Load.Edit
레코드 하나를 만들거나 고치는 page입니다.
page/…/new.tsx, page/…/edit.tsx 같은 곳에 둡니다.Model.Edit
목록 행이나 카드에 붙어 모달을 여는 편집 버튼입니다.
Article.Util.tsx에 둡니다.Model.ViewEditModal
카드를 누르면 상세가 열리고, 그 자리에서 폼으로 바뀝니다.
Article.Zone.tsx에 둡니다.- Template 하나를 세 셸이 함께 씁니다.
st.use.articleForm()만 읽고, 생성인지 수정인지는 따지지 않습니다. - 나머지는 셸이 정합니다. 폼을 무엇으로 채워 열지, 저장한 뒤 어디로 갈지, page로 그릴지 모달로 띄울지가 셸의 몫입니다.
Template은 필드만 그리기
Template은 폼이 어디서 왔는지 판단하지 않습니다. 지금의 폼 상태를 읽고, 필드마다 store setter를 연결할 뿐입니다.
Template이 쓰는 것설명
st.use.articleForm()
폼 상태입니다. 새 레코드면 기본값을, 수정이면 불러온 레코드를 담습니다.
st.do.set<Field>OnArticle
필드마다 하나씩 생기는 setter로, 예를 들면
setTitleOnArticle입니다. onChange에 그대로 넘깁니다.Field.*
라벨과 입력 컨트롤 한 쌍입니다.
Field.Text, Field.TextArea, Field.ToggleSelect, Field.Date 등이 있습니다.제목, 본문, 상태가 있는 article의 Template입니다:
apps/koyo/lib/article/Article.Template.tsx
- 첫 줄은
"use client"이고,useState는 쓰지 않습니다. 값이 모두articleForm에 있어야 생성, 수정, 모달이 이 파일 하나를 함께 씁니다. - enum은
items에 그대로 넘깁니다.Field.ToggleSelect가 값마다 사전의 번역을 라벨로 붙입니다. - 입력값 변환은
transform으로 합니다.Field.Text에transform={(v) => v.toLowerCase()}처럼 주면, setter는 감싸지 않은 채onChange에 그대로 넘길 수 있습니다.


setter는 화살표 함수로 감싸지 말고 그대로 넘기세요.
onChange={(v) => st.do.setTitleOnArticle(v)}는 똑같이 동작하지만 lint에 걸리고, 필드가 에이전트 tool과 data-akan-action을 더 이상 내보내지 않습니다.SSR로 생성 폼 만들기
page가 이미 아는 값은 시작 값 객체(seed)에 담아
Load.Edit에 넘깁니다. parent id, 현재 조직, 기본 상태, URL에서 온 값이 여기에 들어갑니다.게시판 아래의 새 article page입니다. 게시판 값이 채워진 채로 서버에서 렌더링됩니다:
apps/koyo/page/board/[boardId]/article/new.tsx
- seed를 넘기면 새 폼이 열립니다. 빠진 필드는 모델의 기본값을 쓰고, 사용자는 parent id 같은 숨은 값을 고를 필요가 없습니다.
type="form"이면 그 자리에 폼을 그립니다. 저장 버튼도 아래에 붙습니다. 빼면 기본값인 모달로 열립니다.onSubmit은 저장한 뒤 이동할 곳입니다. 경로의[articleId]는 새 레코드의 id로 바뀌므로,"/article/[articleId]"로 두면 방금 만든 글로 갑니다.- 조회해야 아는 값(부모 레코드의 설정 등)은 page에서 await한 뒤 같은 seed에 넣습니다.
수정 페이지
수정 전용 page라면 서버에서 레코드를 불러와
Load.Edit에 넘깁니다. Template은 생성 page와 똑같은 것을 씁니다:apps/koyo/page/article/[articleId]/edit.tsx
fetch.editArticle이 저장된 레코드로 폼을 채웁니다. 저장 버튼 문구도 생성 대신 수정으로 바뀝니다.articleEditpromise는 await하지 않고 넘깁니다. page는 바로 전송되고, 레코드가 도착할 때까지 스켈레톤(또는 넘긴loading)이 폼 자리를 지킵니다.- page가 레코드 자체를 써야 하면 await합니다. 제목이나 URL을 만들 때
const { article, articleEdit } = await fetch.editArticle(articleId)로 받습니다. 이때 page는 레코드가 도착한 뒤에야 전송됩니다.
모달에서 수정하기
사용자가 이미 목록이나 카드를 보고 있다면, 새 page로 옮기기보다 모달에서 고치는 편이 빠릅니다. 모양은 두 가지입니다:
Model.Edit
누르면 폼을 모달로 여는 편집 버튼입니다. 목록 행, 드롭다운, 카드에 둡니다.
<Model.Edit slice modelId renderTitle>Model.ViewEditModal
카드를 누르면 상세 보기가 열리고, 그 안의 편집 버튼이 같은 모달을 폼으로 바꿉니다.
<Model.ViewEditModal slice renderView renderTemplate>Model.Edit — 편집 버튼
article 하나의 편집 버튼과 모달을 그리는 Util입니다:
apps/koyo/lib/article/Article.Util.tsx
- 누르면 레코드를 불러옵니다.
st.do.editArticle(articleId)가 레코드를articleForm에 채우고 모달을 엽니다. - 저장하면 모달이 닫히고, 화면에 떠 있는 모든 목록에서 그 레코드가 갱신됩니다.
renderTitle="title"은 모달 제목을 정합니다. 모델 이름과 폼의title값을 씁니다. 기본 편집 버튼 대신 다른 요소를 쓰려면trigger를 넘깁니다.- 버튼과 모달을 따로 둬야 하나요?
Model.Edit은Model.EditWrapper(트리거)와Model.EditModal id={articleId}(모달)를 합친 것이므로, 둘을 나눠 쓰면 됩니다.
Model.ViewEditModal — 보고 나서 수정
목록 옆에 둔 모달 하나가 목록의 모든 카드를 맡습니다:
apps/koyo/lib/article/Article.Zone.tsx
Model.ViewWrapper가 상세 보기를 엽니다. 카드를 누르면st.do.viewArticle(id)가 불리고, 모달이renderView를 그립니다.- 편집을 누르면
renderTemplate으로 바뀌고, 저장하면 상세 보기로 돌아옵니다. ⋮ 메뉴에는 삭제가 있고,menu={false}로 숨깁니다. renderTitle,editLabel,saveLabel로 모달 제목과 두 버튼의 문구를 바꿉니다.- page가 아니라 Zone에 둡니다.
renderView와renderTemplate은 함수인데, 서버 page는 클라이언트 컴포넌트에 함수를 넘길 수 없습니다.
옵션과 꿀팁
Load.Edit은 아래 props를 Model.EditModal에 그대로 넘기고, Model.EditModal도 같은 props를 받습니다. Model.EditModal에서는 onSubmit, onCancel에 함수도 넘길 수 있습니다.Load.Edit props
sliceSliceMeta
필수입니다.
fetch.slice.<name>으로, 저장할 모델과 새 레코드가 들어갈 목록을 정합니다.editPartial<Model> | ClientEdit
필수입니다. 시작 값 객체를 넘기면 새 폼이,
fetch.edit<Model> 결과를 넘기면 저장된 레코드가 열립니다.type"modal" | "form" | "empty"기본값 "modal"
form은 그 자리에 저장 버튼까지 그리고, empty는 필드만 그립니다.modalstring기본값 "edit"
이 폼을 여는 store의 모달 이름입니다. 같은 모델의 폼이 한 화면에 둘이면 하나에 다른 이름을 줍니다.
onSubmitstring
저장한 뒤 할 일입니다. 경로,
back, reset 중 하나이고, 경로의 [articleId]는 저장된 id로 바뀝니다.onCancelstring
모달을 닫을 때 할 일로, 경로,
back, reset 중 하나입니다. type=form에는 취소 버튼이 없습니다.submitOptionCreateOption
저장 액션에 넘기는 옵션입니다.
{ path: "self" }를 주면 저장된 레코드를 self에도 씁니다.submitTextstring
저장 버튼 문구입니다. 없으면 모델 이름에 생성 또는 수정을 붙인 문구가 나옵니다.
renderSubmitboolean기본값 true
false면 저장 버튼을 숨깁니다. 직접 만든 버튼에서 st.do.submitArticle()을 부르면 됩니다.checkSubmitboolean기본값 true
폼이 모델의 입력 규칙을 통과할 때까지 저장 버튼을 비활성으로 둡니다.
loadingReactNode기본값 Loading.Skeleton
await하지 않은
edit promise가 도착하기 전까지 보여 줍니다.draftboolean | string기본값 true
초안 복구입니다.
false면 끄고, 문자열을 넘기면 그 이름을 범위로 씁니다.classNamestring
래퍼의 class입니다. 모달 창은
modalClassName, 저장 버튼은 submitClassName으로 꾸밉니다.꿀팁
- Template은 하나만 만들어 생성 page, 수정 page, 편집 모달에서 함께 씁니다.
- parent id 같은 숨은 값은 사용자에게 고르게 하지 마세요. 서버에서 준비해 seed에 넣습니다.
- page 이동은
onSubmit, store 갱신은submitOption으로 합니다. 프로필 폼이라면submitOption={{ path: "self" }}로 저장 뒤st.use.self()도 최신으로 맞춥니다. - 필드 로직이 커지면 작은 필드 묶음으로 나누되, 폼의 주인은 Template으로 둡니다.
- 폼 값을 직접 저장하지 마세요. 셸이 입력하는 동안 초안을 보관했다가 다음에 열 때 되돌려 줍니다. 끄려면
draft={false}를 넘깁니다.