Skip to content
created by Aha00aAha00a at 2026-05-22
last modified by Aha00aAha00a at 2026-06-26
revision: 52

Kanban 인터프리터는 위키 텍스트를 단일 원본으로 사용한다.

보기 화면에서는 보드 UI로 편집하고, 저장 시 Kanban 블록을 다시 위키 텍스트로 직렬화한다.

1. Example

=== Info
==== 작업 제목 ==== #cardid123
===== Description
카드에 대한 설명을 여러 줄로 작성할 수 있다.
새 댓글이 추가되어도 밀리지 않는다.
===== Property
 * DueDate: [2027-01-01]
 * Assignee: [User:Aha00a]
 * Attachment: [[Attachment(path/to/file.png)]]
===== Activity
 * [User:Aha00a] [2026-05-04]T10:36:17
  * ["#cardid123" #작업 제목] moved - '''Done''' to '''Info'''
=== ToDo
==== 점심 설거지 ==== #cmq4akx08r020y2
===== Property
 * Creator: [User:Aha00a]
 * dateCreated: [2026-06-07]T21:26:27Z
 * ParentCard: [#cmq4ak09d89cnyp 점심에 할일]
===== Activity
 * [User:Aha00a] [2026-06-07]T21:29:15Z
  * ["#cmq4akx08r020y2" #점심 설거지] updated - ParentCard - [#cmq4ak09d89cnyp 점심에 할일]
 * [User:Aha00a] [2026-06-07]T21:26:27Z
  * ["#cmq4akx08r020y2" #점심 설거지] added
=== Pending
==== 점심 식사 ==== #cmq4aksurw4h9b6
===== Property
 * Creator: [User:Aha00a]
 * dateCreated: [2026-06-07]T21:26:22Z
 * ParentCard: [#cmq4ak09d89cnyp 점심에 할일]
===== Activity
 * [User:Aha00a] [2026-06-07]T21:30:17Z
  * ["#cmq4aksurw4h9b6" #점심 식사] moved - '''ToDo''' to '''Pending'''
 * [User:Aha00a] [2026-06-07]T21:29:10Z
  * ["#cmq4aksurw4h9b6" #점심 식사] updated - ParentCard - [#cmq4ak09d89cnyp 점심에 할일]
 * [User:Aha00a] [2026-06-07]T21:26:22Z
  * ["#cmq4aksurw4h9b6" #점심 식사] added
=== Doing
==== 점심에 할일 ==== #cmq4ak09d89cnyp
===== Property
 * Creator: [User:Aha00a]
 * dateCreated: [2026-06-07]T21:25:45Z
 * BlockedBy: [#cmq4aiz5lnvvyen 일어나기]
===== Activity
 * [User:Aha00a] [2026-06-07]T21:29:36Z
  * ["#cmq4ak09d89cnyp" #점심에 할일] moved - '''ToDo''' to '''Doing'''
 * [User:Aha00a] [2026-06-07]T21:29:28Z
  * ["#cmq4ak09d89cnyp" #점심에 할일] updated - BlockedBy - [#cmq4aiz5lnvvyen 일어나기]
 * [User:Aha00a] [2026-06-07]T21:25:54Z
  * ["#cmq4ak09d89cnyp" #점심에 할일] renamed - '''점심에할일'''
 * [User:Aha00a] [2026-06-07]T21:25:45Z
  * ["#cmq4ak09d89cnyp" #점심에할일] added
==== 점심밥 차리기 ==== #cmq4akmkpy6chrx
===== Property
 * Creator: [User:Aha00a]
 * dateCreated: [2026-06-07]T21:26:14Z
 * ParentCard: [#cmq4ak09d89cnyp 점심에 할일]
===== Activity
 * [User:Aha00a] [2026-06-07]T21:29:57Z
  * ["#cmq4akmkpy6chrx" #점심밥 차리기] moved - '''ToDo''' to '''Doing'''
 * [User:Aha00a] [2026-06-07]T21:29:04Z
  * ["#cmq4akmkpy6chrx" #점심밥 차리기] updated - ParentCard - [#cmq4ak09d89cnyp 점심에 할일]
 * [User:Aha00a] [2026-06-07]T21:26:14Z
  * ["#cmq4akmkpy6chrx" #점심밥 차리기] added
=== Done
==== 일어나기 ==== #cmq4aiz5lnvvyen
===== Property
 * Creator: [User:Aha00a]
 * dateCreated: [2026-06-07]T21:24:57Z
===== Activity
 * [User:Aha00a] [2026-06-07]T21:28:07Z
  * ["#cmq4aiz5lnvvyen" #일어나기] moved - '''ToDo''' to '''Done'''
 * [User:Aha00a] [2026-06-07]T21:24:57Z
  * ["#cmq4aiz5lnvvyen" #일어나기] added
==== 아침에 할일 ==== #cmq4ajpa7tig43b
===== Property
 * Creator: [User:Aha00a]
 * dateCreated: [2026-06-07]T21:25:30Z
 * BlockedBy: [#cmq4aiz5lnvvyen 일어나기]
===== Activity
 * [User:Aha00a] [2026-06-07]T21:28:38Z
  * ["#cmq4ajpa7tig43b" #아침에 할일] moved - '''Doing''' to '''Done'''
 * [User:Aha00a] [2026-06-07]T21:28:15Z
  * ["#cmq4ajpa7tig43b" #아침에 할일] updated - BlockedBy - [#cmq4aiz5lnvvyen 일어나기]
 * [User:Aha00a] [2026-06-07]T21:27:31Z
  * ["#cmq4ajpa7tig43b" #아침에 할일] moved - '''ToDo''' to '''Doing'''
 * [User:Aha00a] [2026-06-07]T21:25:50Z
  * ["#cmq4ajpa7tig43b" #아침에 할일] renamed - '''아침에할일'''
 * [User:Aha00a] [2026-06-07]T21:25:30Z
  * ["#cmq4ajpa7tig43b" #아침에할일] added
==== 아침밥 차리기 ==== #cmq4aj3iyvga6ec
===== Property
 * Creator: [User:Aha00a]
 * dateCreated: [2026-06-07]T21:25:02Z
 * ParentCard: [#cmq4ajpa7tig43b 아침에 할일]
===== Activity
 * [User:Aha00a] [2026-06-07]T21:28:43Z
  * ["#cmq4aj3iyvga6ec" #아침밥 차리기] moved - '''Doing''' to '''Done'''
 * [User:Aha00a] [2026-06-07]T21:27:39Z
  * ["#cmq4aj3iyvga6ec" #아침밥 차리기] moved - '''ToDo''' to '''Doing'''
 * [User:Aha00a] [2026-06-07]T21:26:56Z
  * ["#cmq4aj3iyvga6ec" #아침밥 차리기] updated - ParentCard - [#cmq4ajpa7tig43b 아침에 할일]
 * [User:Aha00a] [2026-06-07]T21:26:34Z
  * ["#cmq4aj3iyvga6ec" #아침밥 차리기] renamed - '''아침밥차리기'''
 * [User:Aha00a] [2026-06-07]T21:25:08Z
  * ["#cmq4aj3iyvga6ec" #아침밥차리기] renamed - '''밥차리기'''
 * [User:Aha00a] [2026-06-07]T21:25:02Z
  * ["#cmq4aj3iyvga6ec" #밥차리기] added
==== 아침 식사 ==== #cmq4ajesg43bp1g
===== Property
 * Creator: [User:Aha00a]
 * dateCreated: [2026-06-07]T21:25:17Z
 * ParentCard: [#cmq4ajpa7tig43b 아침에 할일]
===== Activity
 * [User:Aha00a] [2026-06-07]T21:28:45Z
  * ["#cmq4ajesg43bp1g" #아침 식사] moved - '''Doing''' to '''Done'''
 * [User:Aha00a] [2026-06-07]T21:27:42Z
  * ["#cmq4ajesg43bp1g" #아침 식사] moved - '''ToDo''' to '''Doing'''
 * [User:Aha00a] [2026-06-07]T21:27:13Z
  * ["#cmq4ajesg43bp1g" #아침 식사] updated - ParentCard - [#cmq4ajpa7tig43b 아침에 할일]
 * [User:Aha00a] [2026-06-07]T21:26:45Z
  * ["#cmq4ajesg43bp1g" #아침 식사] renamed - '''아침식사'''
 * [User:Aha00a] [2026-06-07]T21:25:17Z
  * ["#cmq4ajesg43bp1g" #아침식사] added
==== 아침 설거지 ==== #cmq4ajlewxrxne2
===== Property
 * Creator: [User:Aha00a]
 * dateCreated: [2026-06-07]T21:25:25Z
 * ParentCard: [#cmq4ajpa7tig43b 아침에 할일]
===== Activity
 * [User:Aha00a] [2026-06-07]T21:28:48Z
  * ["#cmq4ajlewxrxne2" #아침 설거지] moved - '''Doing''' to '''Done'''
 * [User:Aha00a] [2026-06-07]T21:27:44Z
  * ["#cmq4ajlewxrxne2" #아침 설거지] moved - '''ToDo''' to '''Doing'''
 * [User:Aha00a] [2026-06-07]T21:27:24Z
  * ["#cmq4ajlewxrxne2" #아침 설거지] updated - ParentCard - [#cmq4ajpa7tig43b 아침에 할일]
 * [User:Aha00a] [2026-06-07]T21:26:39Z
  * ["#cmq4ajlewxrxne2" #아침 설거지] renamed - '''아침설거지'''
 * [User:Aha00a] [2026-06-07]T21:25:25Z
  * ["#cmq4ajlewxrxne2" #아침설거지] added

2. Syntax

  • === 리스트명: 컬럼
  • ==== 카드 제목 ==== #cardId: 카드
  • ===== Description: 카드 설명 (선택, 여러 줄, Activity에 밀리지 않음)
  • ===== Property: 카드 속성
  • ===== Activity: 활동 로그

3. 규칙

  • 카드 ID(#...)는 생성 후 변경하지 않는다.
  • 카드/리스트 순서는 입력 순서를 그대로 유지한다.
  • Kanban 블록 외부 텍스트는 저장 시 변경하지 않는다.
  • 저장 전 현재 리비전이 확인되지 않으면 최신 리비전을 1회 조회한 뒤 저장한다.
  • 409 Conflict 발생 시 최신 리비전을 재조회하여 최대 3회 자동 재시도한다. 모두 실패하면 화면 하단에 non-blocking 토스트를 표시하며 페이지는 새로고침하지 않는다.
  • 다른 탭/사용자가 페이지를 저장하면 WebSocket page.updated 이벤트를 수신하여 보드를 자동 갱신한다(실시간 동기화).

4. 실시간 동기화

다른 탭 또는 다른 사용자가 같은 Kanban 페이지를 저장하면 현재 탭의 보드가 자동으로 갱신된다.

  • 다른 세션의 저장을 WebSocket page.updated 이벤트로 감지한다.
  • 최신 원문을 다시 읽고 Kanban 블록을 파싱한 뒤 현재 보드에 병합한다.
  • 저장 중인 변경이 있으면 저장 완료 후 원격 업데이트를 다시 적용한다.
  • 병합 상세와 구현 노트는 Dev Kanban Realtime에 둔다.

4.1. 동작 조건

  • WebSocket이 연결된 상태에서 다른 세션의 저장이 완료되면 page.updated 이벤트를 수신한다.
  • 이벤트 수신 후 500ms debounce 처리 후 GET /w/{page}?action=raw로 최신 원문을 가져온다.
  • 가져온 원문에서 칸반 블록 위치를 재계산하여 파싱한다.

4.2. 병합 전략 (3-way merge)

  • base — 가장 최근에 저장 성공한 시점의 칸반 상태
  • local — 현재 로컬 in-memory 상태 (저장 중인 변경 포함)
  • server — 방금 가져온 서버 상태
로컬만 변경된 카드    → 로컬 버전 유지
서버만 변경된 카드    → 서버 버전 적용
양쪽 모두 변경된 카드 → 서버 우선 + 카드 표면에 노란 conflict 배지
로컬에만 추가된 카드  → 서버 상태에 병합하여 유지
모달에서 열린 카드   → 로컬 버전 유지 + 서버가 변경했으면 모달 상단 배너 표시

4.3. pending 뮤테이션 처리

  • 저장 중인 뮤테이션이 있으면 업데이트를 즉시 적용하지 않고 뮤테이션 완료 후 재적용한다.

4.4. 부수 효과

  • 성공적으로 원격 업데이트를 적용하면 interpreterStartLinelineEnd를 서버 원문 기준으로 재설정한다.
    • * 이후 로컬 저장 시 올바른 줄 번호를 사용하므로 409 발생 가능성이 줄어든다.

5. 속성

허용 속성:

  • Creator (1)
  • DueDate (1)
  • Assignee (n)
  • BlockedBy (n)
  • ParentCard (n)
  • RelatedCard (n)
  • Attachment (n)

5.1. 카드 관계

같은 InterpreterKanban 블록 안의 카드 관계를 Property로 표현한다.

관계 대상은 페이지명을 저장하지 않고 카드 ID hash만 저장한다.

===== Property
 * BlockedBy
  * [#card-api-spec API spec 확정]
 * ParentCard: [#card-login-epic Login Epic]
 * RelatedCard
  * [#card-design-review Design review]
  • 저장값은 [#cardId] 또는 [#cardId label] 형식을 사용한다.
  • 실제 식별자는 cardId이고, label은 원문 가독성을 위한 보조 정보로만 사용한다.
  • 관계 해석 범위는 같은 InterpreterKanban 블록 내부로 제한한다.
  • BlockedBy, ParentCard, RelatedCard를 직접 저장한다.
  • Blocks, SubCards는 저장하지 않고 BlockedBy, ParentCard를 역방향으로 스캔해서 계산 표시한다.

5.2. 카드 표면 관계 배지

보드 뷰의 카드 표면에 관계가 있는 경우 관계 배지를 표시한다.

  • 직접 저장 관계: BlockedBy, ParentCard, RelatedCard
  • 파생 계산 관계: Blocks, SubCards
  • 관계가 없는 카드는 배지 영역을 표시하지 않는다.
  • 관계가 2개 이상인 경우 배지에 개수를 함께 표시한다.
  • 배지와 카드 하단 메타(담당자·통계)는 kanban-card-footer 안에 묶여, 내용이 있을 때만 구분선과 함께 표시된다.
  • 내용이 없는 카드(관계·담당자·댓글·첨부·기한 모두 없음)는 footer 영역이 렌더링되지 않아 불필요한 여백이 생기지 않는다.

5.3. 관계 색상 체계

카드 표면 배지와 카드 상세 모달의 관계 Property 행에 동일한 색상 체계를 적용한다.

관계 타입

색상

BlockedBy

빨간색 (error)

Blocks (파생)

주황색 (warning)

ParentCard

파란색 (primary)

SubCards (파생)

파란색 (primary)

RelatedCard

회색 (muted)

카드 상세 모달에서는 관계 Property 행의 라벨과 카드 칩 테두리·배경에 위 색상을 적용한다.

5.4. 카드 상세 모달 관계 표시

  • 관계 Property별로 현재 연결된 카드를 카드 칩으로 표시한다.
  • 칩에는 최신 카드 제목과 짧은 카드 ID를 표시한다.
  • 칩 클릭 시 openCardDetailById(cardId)로 대상 카드 모달을 연다.
  • 연결 추가는 카드 선택 UI와 수동 ID 입력을 모두 지원한다.
  • 카드 선택 UI는 같은 보드의 카드 목록을 검색/필터링해서 선택한다.
  • 수동 입력은 #cardId, cardId, [#cardId], [#cardId label]을 받아 내부 저장 형식으로 정규화한다.
  • 존재하지 않는 카드 ID는 저장 전 경고하거나, 저장은 허용하되 unresolved 상태로 표시한다.
  • 현재 카드 자신을 관계 대상으로 선택하지 못하게 한다.
  • 관계 삭제 버튼을 제공하고, 삭제 시 card:property:update로 저장한다.

5.5. Assignee

Assignee는 기존 Property 형식을 유지하되 카드 상세 모달에서 직접 편집할 수 있게 한다.

===== Property
 * Assignee
  * [User:Aha00a]
  * [User:Alice]
  • Assignee는 복수 값을 허용한다.
  • 입력값은 User:Aha00a, [User:Aha00a], Aha00a를 받아 [User:Aha00a]로 정규화한다.
  • 현재 Assignee는 사용자 칩으로 표시한다.
  • 현재 사용자와 같은 보드에 등장한 Assignee를 입력 후보로 제공하고, 수동 입력도 허용한다.
  • Assignee 변경은 card:property:update로 저장하고 Activity에 기록한다.
  • 카드 목록에는 Assignee 프로필 이미지 아바타를 표시하고, 이름은 mouseover tooltip으로 제공한다.

6. 모바일 UX

뷰포트 너비가 767px 이하이면 자동으로 모바일 모드로 전환된다.

window.matchMedia('(max-width: 767px)') 이벤트를 구독하여 리사이즈 시에도 동적으로 전환된다.

6.1. 탭 바 + 단일 컬럼 뷰

  • 보드 상단에 컬럼 이름을 탭으로 나열한다.
  • 활성 탭만 해당 컬럼을 표시하고, 나머지 컬럼은 숨긴다.
  • 탭에는 해당 컬럼의 카드 수를 배지로 표시한다.
  • 탭 바 우측 끝의 + 버튼으로 리스트를 추가할 수 있다.

6.2. 컬럼 이동

  • 탭을 탭하여 컬럼을 전환한다.
  • 컬럼 영역을 좌우로 스와이프(50px 이상)하여 인접 컬럼으로 이동한다.

6.3. 드래그 비활성화

  • 모바일에서는 카드/리스트 드래그가 비활성화된다.
  • 카드 이동은 카드 상세 모달의 "List" 셀렉트로 대신한다.

6.4. List 이동 (카드 상세 모달)

  • 카드 상세 모달 Properties 영역 최상단에 "List" 셀렉트를 표시한다.
  • 현재 카드가 속한 컬럼이 기본 선택된다.
  • 다른 컬럼을 선택하면 즉시 이동하고 모달을 닫는다.
  • Activity에 이동 기록이 남으며 card:move로 저장된다.
  • 컬럼이 하나뿐이거나 read-only인 경우 표시하지 않는다.
  • 데스크톱에서도 동일하게 표시된다.

6.5. 바텀시트 모달

  • 카드 상세 모달이 화면 하단에서 올라오는 바텀시트 형태로 표시된다.
  • 높이 최대 90dvh, 상단 모서리 border-radius 16px.

7. 카드 모달

  • 카드 열기 시 URL hash를 #<cardId>로 설정한다.
  • 카드 닫기 시 해당 hash를 제거한다.
  • #<cardId>로 직접 진입하면 해당 카드 모달을 자동으로 열고 보드 위치로 이동한다.
  • 존재하지 않는 #<cardId>는 오류 없이 일반 보드 화면을 유지한다.
  • Property 영역에서 DueDate, Assignee, 카드 관계를 직접 편집할 수 있다.
  • 카드 관계 칩 클릭 시 대상 카드 모달을 연다.
  • 카드 목록에는 Assignee 프로필 이미지 아바타가 표시된다.

8. 리비전 코멘트

형식: Kanban - [주체] 동사과거형 - Detail

  • 카드 이벤트: 카드링크가 주체. ex) Kanban - ["Kanban#cardId" Kanban#할일] moved - '''ToDo''' to '''Done'''
  • 리스트 이벤트: 리스트명(bold)이 주체. ex) Kanban - '''ToDo''' added
  • 지원 액션:
    • list:add, list:rename, list:move, list:delete
    • card:add, card:rename, card:move, card:delete
    • card:comment:add, card:property:update, card:description:update
  • card:comment:add는 코멘트 첫 줄 기준 최대 80자만 기록한다.
  • card:rename의 Detail은 이전 제목(fromTitle). 카드링크 자체가 새 제목을 담고 있으므로 redundancy 없이 변경 내역을 표현한다.
  • card:delete는 카드가 삭제되므로 Activity에는 별도로 기록하지 않는다.

9. Read Only

InterpreterKanban은 일반 위키 페이지와 같은 쓰기 권한 정책을 따른다.

페이지 렌더링 시 계산된 isWritable 값은 .wikiContentdata-wiki-writable 속성으로 전달되고, Kanban UI는 이 값을 기준으로 root별 read-only 모드를 결정한다.

쓰기 권한이 없는 사용자는 Kanban 보드를 조회할 수 있지만, 내용을 변경하는 UI는 노출되지 않는다.

  • 보드, 컬럼, 카드, 속성, Activity, 첨부 이미지는 조회할 수 있다.
  • 카드 상세 모달, 이미지 라이트박스, #<cardId> hash navigation은 동작한다.
  • 보드 배경 드래그를 이용한 가로 스크롤은 read-only에서도 동작한다.
  • list/card 추가, rename, delete, drag sort는 비활성화된다.
  • 댓글 입력, DueDate 수정, 첨부 삭제, 파일 drop 업로드는 비활성화된다.
  • 보드에는 read-only 상태가 표시된다.

클라이언트의 read-only 처리는 UX를 위한 방어선이다.

실제 보안 경계는 서버 저장 로직의 /w/{page} POST 쓰기 권한 검사이며, 쓰기 권한이 없으면 403 Permission denied로 차단된다.

한 페이지에 여러 Kanban interpreter가 있을 수 있으므로 read-only 상태와 mutation 방어는 interpreter root별로 처리한다.

signed read URL로 페이지를 조회하는 경우에도 쓰기 권한이 없으면 read-only로 동작해야 한다.

10. See Also

10.2. Similar Pages

Similar pages by cosine similarity. Words after page name are term frequency.

10.3. Adjacent Pages

Control
≤ 32
all
1.0x
1.0x
80
-120
ON
Metrics
Nodes(visible/total)0/0
Links(visible/total)0/0
Avg degree0.00
Depth coverage0
Queue(fetch/graph)0 / 0
Zoom(scale)1.00x
Ctrl/⌘ + Scroll: Zoom
Root 1-hop 2-hop+