model.Template.tsx

<Model>.Template.tsx는 모듈의 폼입니다. 레코드 하나를 만들거나 고칠 때 사람이 채우는 필드를 그립니다. 대부분 폼 전체를 export하지만, 제출 버튼이나 온보딩 단계, 미리보기 블록 같은 작은 조각을 export해도 됩니다.
Template은 화면을 store에 연결하는 일만 합니다. 판단이 필요한 일은 모두 다른 곳에 둡니다:
할 일
Template
다른 곳
폼 그리기
Field.*
✓
라벨이 붙은 컨트롤로, 각각 폼 초안의 필드 하나에 묶입니다.
제출 버튼 · 단계 · 미리보기
✓
폼 하나에 딸린 작은 인터랙션 조각입니다.
l("<model>.<field>")
✓
모듈 dictionary에서 가져온 라벨과 도움말입니다.
판단과 저장
비즈니스 규칙
✓
검증과 상태 전이는 constant, document, service에 둡니다.
권한 확인
✓
누가 저장할 수 있는지는 signal의 guard가 정합니다.
fetch.*
✓
서버 호출과 그 전후의 토스트는 store 액션에 둡니다.
열기 · 불러오기 · 제출
✓
Template을 감싼 편집 셸이 맡습니다.
✓여기에 둡니다여기에 두지 않습니다
이 페이지에서 쓰는 말
<model>Form
store가 들고 있는, 편집 중인 레코드의 초안입니다. 예를 들면 ticketForm입니다.
st.do.set<Field>On<Model>
store가 필드마다 자동으로 만드는 setter입니다. 예를 들면 setTitleOnTicket입니다.
fetch.slice.<name>
Field나 셸에게 어떤 모델의 어떤 목록을 다루는지 알려 줍니다.
편집 셸
Load.Edit, Model.Edit처럼 폼을 불러오고, 열고, 제출하는 래퍼입니다.

파일 규칙

Template은 편집할 모델과 같은 모듈 폴더에 둡니다. 모든 필드가 브라우저에만 있는 store를 읽고 쓰므로, 첫 줄은 언제나 "use client"입니다.
경로
apps/<app>/lib/<model>/<Model>.Template.tsx
데이터베이스 모듈과 스칼라 모듈에 둘 수 있고, 서비스 모듈에는 두지 않습니다.
첫 줄
"use client";
언제나 import보다 먼저, 파일 첫 줄에 씁니다.
export
General · Phone · SubmitPhone · PhoneCode
이름 있는 화살표 함수 컴포넌트입니다. General이 모델의 기본 폼입니다.
쓰는 모양
<Ticket.Template.General />
page와 셸은 @apps/<app>/client의 모델 네임스페이스로 가져다 씁니다.
  • 기본 폼의 이름은 General로 합니다. Model.AdminPanel은 Template.General을 폼으로 쓰고, 없으면 첫 번째 export를 씁니다.
  • Template에는 useState를 쓰지 않습니다. 폼 값은 store에 둡니다. Template 안의 useState는 akan quality ssr이 akan.ssr.template-client-state 경고로 잡아냅니다.

기본 폼 Template

기본 폼은 store에서 초안을 읽고, 라벨은 dictionary에서 가져오며, 필드마다 자동 생성된 setter로 값을 씁니다:
apps/koyo/lib/ticket/Ticket.Template.tsx
  • st.use.ticketForm()이 초안을 읽습니다. 초안이 바뀔 때마다 폼이 다시 렌더링됩니다.
  • label과 desc는 dictionary key입니다. desc는 라벨 옆 도움말 툴팁으로 보입니다.
  • st.do.setTitleOnTicket은 자동으로 생깁니다. store가 필드마다 setter를 하나씩 만들어 두므로 그대로 넘기기만 하면 됩니다.
  • Layout.Template이 필드 간격을 고르게 맞춥니다. 필요하면 호출하는 쪽에서 className으로 조정합니다.

Field 고르기

Field.* 컴포넌트는 라벨 행까지 갖춰 미리 만들어 둔 폼 컨트롤입니다. 모델 필드에 맞는 것을 고르고 value와 onChange를 store에 연결합니다.
모델 필드Field
↳ 참고
StringField.Text
특수한 텍스트에는 TextArea, Email, Phone, Password를 씁니다.
Int · FloatField.Number
범위처럼 숫자 두 개를 한 줄에 받을 때는 DoubleNumber를 씁니다.
BooleanField.Switch
라벨이 붙은 켜고 끄는 토글입니다.
DateField.Date
showTime을 켜면 시각까지 받고, 기간은 DateRange로 받습니다.
enumOf(...)Field.ToggleSelect
값마다 번역된 라벨이 붙고, 배열이면 MultiToggleSelect를 씁니다.
[String]Field.Tags
순서가 중요하면 행을 드래그할 수 있는 TextList를 씁니다.
다른 모델과의 관계Field.Parent
배열이면 Children, ID 필드면 ParentId / ChildrenId를 씁니다.
FileField.Img
[File]이면 Imgs, 이미지가 아니면 File / Files를 쓰며 모두 @libs/shared/ui에 있습니다.
서식 있는 본문Field.Rich
첨부 파일을 넣을 수 있는 리치 텍스트 에디터이며, @libs/shared/ui에 있습니다.
내장 객체 목록Field.List
행 하나만 그리면 추가·삭제 버튼은 필드가 그립니다.
기본 멤버는 akanjs/ui에 있습니다. @libs/shared/ui의 Field는 이를 모두 담고 Rich, Img, Imgs, File, Files, Coordinate, Postcode를 더하므로 여기서 가져옵니다. 아래는 value와 onChange 말고도 prop이 더 필요한 필드 네 개를 같은 폼에 추가한 모습입니다:
apps/koyo/lib/ticket/Ticket.Template.tsx
  • Field.Parent는 관련 모델 하나를 고릅니다. 선택지는 slice의 목록에서 오고, renderOption이 각 선택지를 그립니다.
  • Field.ToggleSelect는 items에 enumOf 클래스를 받습니다. 값마다 번역된 버튼이 하나씩 생깁니다.
  • Field.Img는 slice를 통해 업로드합니다. 올라간 File을 폼에 담고, nullable을 주면 라벨에 선택 항목 표시가 붙습니다.
  • Field.Rich에는 폼 안의 필드 key인 valuePath가 꼭 필요합니다. addFile은 에디터에서 올린 파일을 받으며, 여기서는 [File] 필드에 자동 생성된 add<Field>On<Model>을 넘겼습니다.
맞는 Field가 없으면 akanjs/ui의 Input이나 버튼, 앱 전용 컴포넌트를 써도 괜찮습니다. 다만 모델 필드에 맨 <input>을 쓰지는 않습니다. 모든 멤버의 prop 목록은 이 페이지 끝에 연결한 "폼 컨트롤" 문서에 있습니다.

컴포넌트 나누기

Template 하나가 작은 컴포넌트 여러 개를 export해도 됩니다. 큰 폼은 모든 것을 General에 몰아넣지 말고, 업무 단계나 UI 역할에 따라 나눕니다.
libs/shared의 user 모듈은 휴대폰 가입 단계를 입력칸과 인증번호를 보내는 버튼으로 나눕니다. 핵심만 간추리면 다음과 같습니다:
libs/shared/lib/user/User.Template.tsx
  • 조각마다 export를 하나씩 둡니다. page는 <User.Template.Phone />과 <User.Template.SubmitPhone />을 레이아웃에 맞는 자리에 따로 놓습니다.
  • 조각끼리는 store로 상태를 나눕니다. 둘 다 st.use.phone()을 읽으므로 서로 prop을 주고받지 않습니다.
  • setter는 여기서도 참조로 넘깁니다. store의 setPhone이 번호 형식을 직접 맞추므로 입력칸에 래퍼가 필요 없습니다.
  • 인자가 필요한 호출은 화살표 함수로 씁니다. onPressEnter와 onClick은 userId와 phone을 store 액션에 넘기고, 결과를 기다리지 않도록 void를 붙입니다.

Template 여는 법

Template은 필드를 그리기만 합니다. 폼 상태를 채우고, 폼을 열고, 제출하는 일은 Template을 감싼 편집 셸이 합니다. 셸은 폼이 열리는 자리에 따라 고릅니다:
셸이럴 때 씁니다그리는 것
Load.Editpage가 편집할 레코드나 새 폼의 일부 값을 이미 가지고 있을 때type에 따라 page 안의 폼이나 모달, 또는 필드만 그립니다.
Model.Edit목록의 행, 드롭다운, Unit에 편집 버튼이 필요할 때편집 버튼(또는 넘긴 trigger)과 편집 모달입니다.
Model.New화면에 레코드를 새로 만드는 버튼이 필요할 때새로 만들기 버튼(또는 넘긴 trigger)과 폼 모달입니다.
Model.NewWrapper빈 목록 안내 버튼처럼 아무 요소나 새 폼을 열어야 할 때트리거만 그리므로 Model.EditModal과 짝지어 씁니다.
  • 셸이 초안을 보관하므로, 폼 값을 직접 저장하지 마세요. 사용자가 입력하는 동안 폼을 저장해 두었다가 다음에 열 때 되돌려 줍니다. 끄려면 draft={false}를 넘기고, 레코드 id나 시작 값만으로 구분되지 않는 폼이면 draft="<scope>"로 범위를 직접 정합니다.
page에서 Load.Edit 쓰기
page가 무엇을 편집할지 이미 알고 있으면 Load.Edit을 씁니다. 안에 든 Template은 그대로 클라이언트 컴포넌트입니다. 새 레코드라면 edit에 일부 값만 채운 모델을 넘깁니다:
apps/koyo/page/ticket/new.tsx
이미 있는 레코드를 고칠 때는 fetch.edit<Model>로 edit 객체를 가져와 대신 넘깁니다:
apps/koyo/page/ticket/[ticketId]/edit.tsx
  • type이 폼이 나타날 자리를 정합니다. "form"이면 page 안에 제출 버튼과 함께, "empty"면 필드만 그립니다. 기본값 "modal"이면 모달로 그립니다.
  • onSubmit과 onCancel은 "back", "reset", 경로 중 하나를 받습니다. 경로 안의 [ticketId]는 저장된 레코드의 id로 바뀝니다.
  • edit에는 await하지 않은 promise를 넘겨도 됩니다. const { ticketEdit } = fetch.editTicket(ticketId)로 넘기면 스트리밍되고, 도착할 때까지 skeleton(또는 넘긴 loading)이 보입니다.
Template이 렌더링되기 전에 Load.Edit이 store에 쓰는 key는 다음과 같습니다:
상태 keyedit 객체를 넘기면부분 폼을 넘기면
<model>edit 객체로 만든 full 모델입니다.null
<model>Loadingfalse그대로 둡니다.
<model>Form모델을 복사한 편집용 폼입니다.기본값에 edit을 합친 폼입니다.
<model>FormLoadingfalsefalse
<model>Modalmodal prop, 없으면 "edit"입니다.modal prop, 없으면 "edit"입니다.
<model>ViewAt서버가 레코드를 읽은 시각으로, 오래된 값이면 다시 읽는 데 씁니다.그대로 둡니다.
편집 모달은 Model.Edit
목록의 행, 드롭다운, Unit에 편집 버튼이 필요하면 Model.Edit을 씁니다. 버튼과, Template을 담은 모달을 함께 그립니다:
apps/koyo/lib/ticket/Ticket.Util.tsx
  • 클릭하면 레코드를 불러옵니다. 버튼이 st.do.editTicket(ticketId)를 호출해 레코드를 폼에 채우고 모달을 엽니다.
  • renderTitle="title"은 모달 제목을 정합니다. 모델 이름과 폼의 title 값을 씁니다. 기본 편집 버튼 대신 다른 요소를 쓰려면 trigger를 넘깁니다.
새 폼 열기는 Model.NewWrapper
Model.NewWrapper는 아무 요소나 새 폼을 여는 버튼으로 만듭니다. 트리거만 그리므로, Template은 같은 slice의 Model.EditModal이 그립니다:
apps/koyo/lib/ticket/Ticket.Zone.tsx
  • partial이 폼의 시작 값이 됩니다. 클릭하면 자동 생성된 st.do.newTicket()이 이 값으로 폼을 채웁니다.
  • Model.New는 이 한 쌍을 컴포넌트 하나로 묶은 것입니다. 트리거와 모달이 서로 다른 자리에 있을 때 Model.NewWrapper를 씁니다.

한눈에 보는 규칙

앞에서 다룬 내용을 Template을 마무리하기 전에 확인할 목록으로 모았습니다:
  • 첫 줄에는 언제나 "use client"를 씁니다. Template은 브라우저에만 있는 store를 읽습니다.
  • 필드는 Layout.Template으로 감쌉니다. 그래야 모든 폼의 간격이 같습니다.
  • 라벨은 모두 dictionary에서 가져옵니다. label={l("ticket.title")}, desc={l("ticket.title.desc")}처럼 쓰고, 문구를 직접 적지 않습니다.
  • 자동 생성 setter는 참조로 넘깁니다. 값을 다듬을 때는 setter를 화살표 함수로 감싸지 말고 Field의 transform을 씁니다.
  • 폼 값은 useState가 아니라 store에 둡니다. 폼은 st.use.<model>Form()으로 읽습니다.
  • 맞는 Field가 없으면 일반 컨트롤을 씁니다. Input, 버튼, 직접 만든 컴포넌트 모두 괜찮지만, 모델 필드에 맨 <input>을 쓰지는 않습니다.
  • 비즈니스 판단은 Template 밖에 둡니다. constant, document, service, signal, store 액션으로 옮기고, Template에서는 fetch.*를 부르지 않습니다.
  • 큰 폼은 이름 있는 컴포넌트로 나눕니다. General, Phone, SubmitPhone처럼 나눕니다.
  • 폼은 셸이 열게 합니다. 데이터가 있는 page에는 Load.Edit, 편집 모달에는 Model.Edit, 새 폼 버튼에는 Model.New나 Model.NewWrapper를 씁니다.
이어서 읽기

MIT 라이선스 하에 배포되었습니다.

내 AI에 이 문서 연결하기

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman