사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
CLI 레퍼런스▾
AkanJS 레퍼런스▾
UI 레퍼런스▾

표시 UI

데이터를 보여 주는 컴포넌트입니다. 모델 목록, 시간, 로딩·빈 상태, 상태 배지, 표와 pager가 있고 모두 akanjs/ui에서 가져옵니다.import { Badge, Data, Empty, Loading, Pagination, RecentTime, Table } from "akanjs/ui";
Data
모델 하나의 slice에 묶인 admin 목록 화면과 그 부품들입니다.
RecentTime
시간을 "3분 전"처럼 보여 주고, 정확한 날짜는 툴팁으로 띄웁니다.
Loading
기다리는 동안 보여 줄 표시 여섯 가지입니다. spinner, skeleton, 진행률 막대 등이 있습니다.
Badge
상태를 보여 주는 작은 배지입니다. badgeRecipe로 꾸민 <span>입니다.
Empty
"데이터 없음" 자리 표시입니다. 아래에 다음 행동 버튼을 둘 수 있습니다.
Table
이미 가진 행 데이터를 표로 그립니다. pager를 붙일 수 있습니다.
Pagination
prop만으로 동작하는 페이지 번호 control입니다.
이 페이지에서 쓰는 말
slice
모델 목록 하나와 그 목록이 채우는 store key를 가리키는 메타데이터입니다. fetch.slice.<model>로 꺼냅니다.
store
모델마다 생성되는 클라이언트 상태입니다. st.use로 읽고 st.do로 바꿉니다.
insight
slice가 행과 함께 돌려주는 집계 값입니다. count가 대표적입니다.
override slot
route의 _overrides.tsx가 앱 자체 컴포넌트로 바꿔 끼울 수 있는 이름입니다.
store에서 읽을까, prop으로 받을까
pager와 표는 각각 두 가지 버전이 있고, 둘을 바꿔 쓰는 것이 흔한 실수입니다. 모델 slice에는 Data.* 쪽을, 이미 가진 값에는 일반 컴포넌트를 씁니다:
컴포넌트
store
props
페이지 넘기기
Data.Pagination
✓
slice의 현재 페이지·페이지 크기·전체 개수를 store에서 읽습니다.
Pagination
✓
currentPage, total, itemsPerPage를 prop으로 받습니다.
표
Data.TableList
✓
모델에 연결된 목록입니다. 행, pager, modal이 모두 slice에서 옵니다.
Table
✓
이미 가진 행을 dataSource로 넘깁니다.
✓값을 가져오는 곳쓰지 않음
함께 볼 페이지

Data

admin 목록 화면을 부품으로 나눈 것입니다. Data.ListContainer가 화면 전체이고, 나머지 멤버는 그 조각입니다. 배치를 바꾸고 싶을 때 fork하지 않고 조합하도록 따로 공개되어 있습니다.
모든 멤버는 같은 slice를 받습니다. 어떤 모델과 어떤 store key를 읽을지 알려 주는 값입니다.
멤버
Data.ListContainer{ slice, type?, query?, columns?, actions?, tools?, render…? }
admin 목록 화면 전체입니다. toolbar, dashboard, 표나 카드 목록, CRUD modal을 모두 그립니다.
Data.TableList{ slice, columns, init?, queryArgs?, actions?, renderView?, renderTemplate?, renderTitle?, onItemClick? }
목록을 행으로 그리고 edit·view modal도 직접 띄웁니다. queryArgs를 주면 마운트할 때 직접 불러옵니다.
Data.CardList{ slice, columns, renderItem, init?, actions?, renderView?, renderTemplate?, renderLoading? }
목록을 카드로 그립니다. renderItem 결과는 행 action이 달린 Data.Item 안에 놓입니다.
Data.Item{ slice, model, title?, actions?, columns?, onClick?, children? }
카드 하나입니다. 위에 children(없으면 title), 아래에 column 값과 action 버튼이 옵니다.
Data.Pagination{ slice, className? }
pager입니다. 페이지 상태를 prop이 아니라 slice의 store에서 읽습니다.
Data.Dashboard{ slice, summary, columns?, presents?, hidePresents?, queryMap?, summaryRefName?, onSelect?, queryKey? }
목록 위의 요약 타일입니다. 자기 filter를 아는 타일은 누르면 목록을 좁힙니다.
Data.Insight{ slice, insight, columns? }
slice의 insight 값을 타일로 보여 줍니다. 전체 개수는 이미 header에 있습니다.
Data.QueryMaker{ slice, query?, onApply? }
선언된 filter를 고르고 인자를 채웁니다. onApply의 기본값은 slice의 store입니다.
Data.RefPicker{ refName, value, onChange }
filter 인자의 ref가 가리키는 모델에서 행을 골라 id를 채웁니다. owner id가 대표적입니다.
Data.ListContainer props
sliceSliceMeta
모델의 root slice인 fetch.slice.<model>입니다.
type"card" | "list"기본값 "card"
처음 보여 줄 모양입니다. toolbar 토글로 카드와 표를 오갑니다.
queryQuerySetting
filter를 고정합니다. 이때 query maker와 dashboard는 그리지 않습니다.
queryMap{ [column]: QuerySetting }
요약 column마다 적용할 filter입니다. ?filter=<column>으로 열면 그 filter로 시작합니다.
initFetchInitForm
첫 조회 설정입니다. page, limit, sort와 새 모델의 기본값을 담습니다.
columnsDataColumn[]기본값 ["id", "createdAt", "updatedAt"]
행과 카드에 보여 줄 field입니다. CSV 내보내기도 이 목록을 씁니다.
actionsDataAction[] | (item, idx) => DataAction[]기본값 ["remove", "edit", "view"]
행마다 붙는 버튼입니다. 함수로 주면 행마다 따로 정합니다.
toolsDataTool[] | (list) => DataTool[]기본값 []
toolbar 더보기 메뉴에 CSV·JSON 내보내기와 함께 들어갈 항목입니다.
createboolean기본값 true
새로 만들기 버튼을 보여 줍니다. renderTemplate이 있을 때만 나타납니다.
titleReactNode
제목입니다. 기본값은 dictionary에 적힌 모델 이름입니다.
sortsort key
처음 정렬 기준입니다. toolbar에서 모델의 다른 정렬 기준으로 바꿀 수 있습니다.
classNamestring
컨테이너 전체에 붙일 class입니다.
cardListClassNamestring
카드 grid에 붙일 class입니다.
render slot
renderItem(props) => ReactNode
카드 모드의 카드 본문입니다. { [model]: item, slice, actions, columns, idx }를 받습니다.
renderTemplate(props) => ReactNode
edit·new modal 안의 form입니다. 없으면 새로 만들기 버튼도 없습니다.
renderView(model) => ReactNode
view modal의 본문입니다. 없으면 view 버튼을 눌러도 아무것도 열리지 않습니다.
renderTitle(model) => ReactNode
modal 제목입니다. 기본값은 모델 이름과 id입니다.
renderDashboard({ summary, onSelect, queryKey, hidePresents }) => ReactNode
목록 위 영역으로, 보통 Data.Dashboard를 둡니다. 앱의 summary 상태가 있어야 그려집니다.
renderInsight({ insight }) => ReactNode
목록 위의 insight 영역으로, 보통 Data.Insight를 둡니다.
renderQueryMaker() => ReactNode
toolbar 아래의 filter 인자 입력란을 바꿔 그립니다.
renderLoading() => ReactNode
카드를 불러오는 동안 반복해서 보여 줄 자리 표시 카드 하나입니다.
column
"name"
field 이름입니다. header 이름은 모델 dictionary에서 가져옵니다.
"createdAt""updatedAt""startAt"
이런 이름의 날짜 field는 RecentTime으로 그립니다.
"status""role"
이름에 status나 role이 들어가면 색 배지로 그립니다.
{ key, title?, render?, value?, responsive? }
header와 cell을 직접 정합니다. CSV 내보내기는 render 대신 value의 결과를 씁니다.
{ key, responsive: true }
md 이상 화면에서만 column을 보여 주고, 그보다 작은 화면에서는 숨깁니다.
action과 tool
"view""edit""remove"
store에 연결된 아이콘 버튼입니다. remove는 먼저 확인을 받습니다.
<YourButton />
직접 만든 element입니다. 표에서는 Actions column에, 카드에서는 더보기 메뉴에 들어갑니다.
(item, idx) => DataAction[]
행마다 버튼을 정합니다. 예를 들어 상태에 따라 다르게 줄 수 있습니다.
{ key, render }
tools 항목입니다. tools를 함수로 주면 불러온 목록을 받습니다.
filter와 dashboard 타일
  • query maker는 모델에 선언된 filter를 나열합니다. 모델 타입 인자를 받는 filter는 빠지고, ref가 모델을 가리키는 id 인자에는 Data.RefPicker가 붙습니다.
  • 필수 인자가 채워질 때까지 기다립니다. 필수 인자가 모두 채워져야 요청을 보내고, 입력 중에는 debounce가 걸립니다.
  • 자기 query를 아는 타일만 filter가 됩니다. queryMap[column]이 우선이고, 없으면 summary field의 .meta({ refName, queryKey, queryArgs })가 이 모델을 가리킬 때 씁니다. onSelect가 없으면 모든 타일이 그냥 표시만 합니다.
  • queryKey로 활성 타일 표시가 정확해집니다. 목록이 보여 주는 filter이므로, toolbar에서 다른 filter로 옮기면 타일의 활성 표시가 풀립니다.
예시
표 모양으로 시작하고, 행 action마다 modal이 연결된 상품 admin 목록입니다:
apps/koyo/lib/product/Product.Zone.tsx
  • root slice만 받습니다. fetch.slice.product를 넘기세요. productInOrg 같은 이름 있는 slice를 넘기면 에러가 납니다. 범위는 query로 좁힙니다.
  • action마다 짝이 되는 slot을 줍니다. renderTemplate이 edit modal을 채우고 새로 만들기 버튼을 띄우며, renderView가 view modal을 붙입니다.
  • Model.AdminPanel이 이 연결을 대신 해 줍니다. 모듈의 Unit, Template, View namespace를 받아 이 slot들을 채웁니다.

RecentTime

시간을 페이지 언어에 맞춰 "3분 전" 같은 상대 표기로 보여 주고, 정확한 날짜는 툴팁으로 띄웁니다. breakUnit을 넘어선 시간은 날짜로 찍습니다.
Props
dateDate | Dayjs | null
보여 줄 시간입니다. null이면 아무것도 그리지 않습니다.
breakUnitIntl.RelativeTimeFormatUnit
상대 표기를 멈출 단위입니다. 주지 않으면 날짜로 바뀌지 않습니다. 아래 표를 보세요.
format"auto" | "full"기본값 "auto"
상대 표기를 넘어선 날짜를 찍는 방식입니다. 아래 표를 보세요.
relative"fromNow" | "always" | "auto" | (ctx) => string기본값 "fromNow"
상대 표기의 문구입니다. 아래 표를 보세요.
classNamestring
label에 붙일 class입니다.
상대 표기가 멈추는 지점
breakUnit상대 표기 구간
지정 안 함항상 상대 표기이며 날짜로 바뀌지 않음
"second"상대 표기 없이 항상 날짜
"minute"60초 미만
"hour"60분 미만
"day"24시간 미만
"week"7일 미만
"month"4주 미만
"year"12개월 미만
찍히는 모양
경우표시
상대 표기 이후, 같은 날HH:mm
상대 표기 이후, 같은 해MM-DD
상대 표기 이후, 다른 해YYYY-MM-DD
상대 표기 이후, format="full"일 때YY-MM-DD HH:mm
툴팁YYYY-MM-DD HH:mm
툴팁, breakUnit="second"일 때YYYY-MM-DD HH:mm:ss
epoch 자리 값(0 또는 -1)--:--
상대 표기 문구
relative하루 전 날짜의 출력
↳ 문구 출처
"fromNow"하루 전
dayjs locale 문자열입니다. 기본값입니다.
"always"1일 전
Intl.RelativeTimeFormat으로, 항상 숫자로 씁니다.
"auto"어제
Intl.RelativeTimeFormat으로, 어제처럼 말로 된 표현이 있으면 그것을 씁니다.
(ctx) => string…
{ unit, count, date, now, defaultLabel }를 받아 문구를 직접 만듭니다.
예시
"하루 전" 대신 "어제"라고 쓰고, 일주일이 지나면 날짜를 찍는 글 작성 시간 표시입니다:
apps/koyo/lib/story/Story.View.tsx
  • 서버 컴포넌트에서 그대로 씁니다. 위 View에는 "use client"가 없습니다.
  • breakUnit은 날짜로 찍히기 시작하는 단위입니다. "week"면 일주일 미만은 상대 표기로, 그보다 오래된 시간은 날짜로 찍힙니다.

Loading

기다리는 대상의 모양마다 하나씩, 표시가 여섯 가지 있습니다. 사용자가 보고 있는 것에 맞춰 고르세요:
기다리는 것쓸 것
곧 내용이 들어올 자리Loading.Skeleton
control이나 작은 영역이 동작 중일 때Loading.Spin
업로드처럼 끝이 정해진 작업Loading.ProgressBar
패널 전체가 작업 중일 때Loading.Area
버튼이나 입력란이 아직 없을 때Loading.Button · Loading.Input
멤버
Loading.Spin{ className?, indicator?, isCenter?, size?: "sm" | "md" | "lg" | number, tone? }기본값 size "md", tone "primary"
spinner입니다. size는 단계 이름이나 픽셀, tone은 "primary", "current", "muted"입니다.
Loading.Skeleton{ className?, active?, style? }기본값 active true
회색 글줄 네 개이며 active인 동안 깜빡입니다. Load.Stream의 fallback에 알맞습니다.
Loading.ProgressBar{ className?, value, max }
value / max까지 움직이는 진행률 막대입니다. 두 값이 모두 실제 숫자일 때 씁니다.
Loading.Button{ className?, active?, style? }기본값 active true
아직 없는 버튼 자리의 placeholder입니다. 버튼 안에 넣는 spinner가 아닙니다.
Loading.Input{ className?, active?, style? }기본값 active true
같은 placeholder를 입력란 모양으로 그립니다.
Loading.Area{ className?, indicator?, children? }
spinner와 메시지(기본값은 처리 중)를 얹은 반투명 absolute inset-0 막입니다.
예시
spinner와 진행률 막대를 함께 둔 업로드 줄입니다:
apps/koyo/ui/UploadProgress.tsx
  • 채워진 표면 위에서는 tone="current"를 줍니다. 기본 text-primary/70은 bg-info 배지나 primary 버튼 위에서 보이지 않습니다. className의 text-*는 어떤 tone보다 우선합니다.
  • 바꿔 넣은 아이콘에는 회전 class가 필요 없습니다. indicator는 자기 색을 유지하고, SVG 아이콘이면 wrapper가 대신 돌려 주므로 animate-spin을 빼세요.
  • 덮는 표시는 위치가 지정된 부모가 필요합니다. Loading.Area와 Loading.Spin isCenter는 absolute inset-0이므로 relative 요소 안에 둡니다.
  • 멤버마다 별도의 override slot입니다. LoadingSpin은 그대로 두고 LoadingSkeleton만 바꿔 입힐 수 있고, 나머지 넷도 마찬가지입니다.

Badge

상태 배지입니다. <span>에 badgeRecipe variant를 얹은 것이 전부이고, 나머지 속성은 그대로 전달되므로 title, aria-*, click handler가 모두 동작합니다.
Props
variant"default" | "primary" | "secondary" | "accent" | "neutral" | "success" | "warning" | "info" | "error" | "outline"기본값 "default"
색입니다. 모델 enum은 module scope의 as const 표로 이어 줍니다.
size"xs" | "sm" | "md" | "lg"기본값 "md"
높이와 글자 크기입니다.
outlineboolean
variant의 색을 외곽선으로 그립니다. variant="outline"은 색이 없는 기본 외곽선입니다.
...HTMLAttributes<HTMLSpanElement>attributes
<span>이 받는 모든 속성입니다. className은 마지막에 병합되어 variant보다 우선합니다.
예시
작업 상태 배지입니다. enum 값은 module scope 표를 거쳐 variant로 바뀝니다:
apps/koyo/lib/job/Job.Unit.tsx
  • 모든 배지를 한 번에 바꿀 수 있습니다. route의 _overrides.tsx에 recipes: { badge }를 연결하면 호출부는 하나도 고치지 않아도 됩니다.
  • class만 필요하면 badgeRecipe를 부릅니다. badgeRecipe(variants, className)로 <a>나 <button>에 배지 모양을 입힙니다. 서버에서도 안전하고 두 번째 인자로 배열도 받으므로 cn()으로 감싸지 않습니다.

Empty

표준 "데이터 없음" 화면입니다. 아이콘과 번역된 문구를 보여 주고, 아래에 다음 행동을 둘 수 있습니다.
Props
descriptionReactNode기본값 l("base.noData")
빈 상태 문구입니다. 기본값은 번역된 '데이터 없음' 문구입니다.
iconReactNode
문구 위의 아이콘입니다. 기본값은 받은편지함 아이콘입니다.
minHeightnumber기본값 300
빈 상태 영역의 최소 높이(px)입니다.
classNamestring
빈 상태 영역에 붙일 class입니다. children은 이 영역 바깥에 놓입니다.
childrenReactNode
빈 상태 영역 아래에 둘 내용입니다. 만들기 버튼이 대표적입니다.
예시
빈 상태에서 만들기 버튼을 보여 주는 상품 목록입니다. Load.Units에 넘깁니다:
apps/koyo/lib/product/Product.Zone.tsx
  • 직접 붙일 일은 드뭅니다. Load.Units는 행이 없으면 이미 <Empty />를 그리고, Table도 160px 높이로 하나 그립니다. 바꾸고 싶을 때만 직접 넘깁니다.
  • override 하나로 전부 바뀝니다. Empty는 override slot이라, route의 _overrides.tsx가 그 아래 모든 빈 상태를 바꿉니다.

Table

이미 가진 행을 그리는 반응형 표입니다. store에 연결된 모델 목록이라면 Data.TableList를 씁니다.
Props
columns{ key?, title, dataIndex, render?, responsive? }[]
column마다 header와 cell을 정합니다. responsive에는 보일 breakpoint를 적습니다.
dataSourceany[]
그릴 행 전부입니다. 현재 페이지만큼 자르는 일은 직접 합니다.
rowKey(row) => string
행마다 쓸 React key입니다. 기본값은 행 번호입니다.
loadingboolean
행을 흐리게 하고 그 위에 loadingIndicator를 그립니다.
loadingIndicatorReactNode
loading 동안 행 위에 보이는 표시입니다. 기본값은 spinner입니다.
paginationPaginationProps | false
표 아래에 Pagination을 그립니다. 없거나 false면 그리지 않습니다.
onRow(record, index) => { onClick }
클릭해서 열기 같은 행 이벤트입니다. 이때 행에 pointer 커서가 붙습니다.
rowClassNamestring | (record, index) => string
모든 행, 또는 행마다 붙일 class입니다.
size"small" | "middle"
"small"은 cell 위아래 여백을 줄입니다.
borderedboolean
표 둘레에 둥근 테두리를 그립니다.
showHeaderboolean | Responsive[]기본값 true
header를 숨기거나, 적은 breakpoint에서만 보여 줍니다.
headerReactNode
표 위에 그릴 내용입니다.
footerReactNode
표 아래, pager 밑에 그릴 내용입니다.
emptyReactNode기본값 <Empty minHeight={160} />
행이 없을 때의 placeholder입니다.
예시
페이지를 로컬에서 넘기고, 작은 화면에서는 금액 column을 숨기는 청구서 표입니다:
apps/koyo/ui/InvoiceTable.tsx
  • Table은 행을 잘라 주지 않습니다. dataSource를 전부 그리므로, 위처럼 현재 페이지만 넘깁니다.
  • pagination은 네 값만 넘깁니다. currentPage, total, itemsPerPage, onPageSelect만 pager에 전달됩니다. prev, next, empty가 필요하면 pagination={false}로 두고 footer에 Pagination을 직접 그립니다.

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

내 AI에 이 문서 연결하기

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