UI 선언 시스템
03 만드는 법
이 문서 안에서
게임 화면은 ui/ 폴더의 선언 파일 25개에 들어 있습니다. 이 문서는 파일 문법, 렌더러 절차, 테마 토큰, 새 화면 추가 절차를 정합니다. 마운트 시점과 오버레이 생명주기는 화면 구성이 다룹니다.
한 줄 요약 — 마크업과 CSS 는
ui/*.ui.json에, TypeScript 는 데이터 바인딩과 이벤트만 — 두 세계를 잇는 유일한 계약은ref문자열입니다.
왜 JSON 인가
마켓 목업(src/market/)이 shared만 import 할 수 있어 렌더러가 src/shared/ui/에 있어야 합니다. 그 목업 렌더러가 같은 .ui.json을 같은 헬퍼로 써서 스크린샷과 실제 화면이 일치합니다(마케팅 소재). JSON에 함수를 담을 수 없으므로 events 사용 횟수는 0입니다.
문서 하나의 모양
파일 하나 = UIDefinition(src/shared/types/ui.ts). 키 순서: name→css→templates→ui.
| 필드 | 필수 | 뜻 |
|---|---|---|
name | 필수 | 문서 이름. 빈 불허. 파일명과 동일 |
ui | 필수 | 루트 노드 하나 |
css | 선택 | 문서 전용 스타일시트. 자동 스코핑 |
templates | 선택 | 리스트 바인딩용 조각 |
css는 한 줄 JSON 문자열. resolveJsonModule static import → src/ui-json.d.ts가 타입 지정. 빌드 시점 번들.
노드 속성 목록
재귀 타입 UINode(src/shared/ui/dom/types.ts).
| 속성 | 뜻 | 사용 횟수 |
|---|---|---|
type | 태그 이름 | 881 |
class | 문자열 또는 문자열 배열 | 818 |
ref | TS 가 이 노드를 찾는 이름 | 436 |
text | textContent | 397 |
children | 자식 노드 배열 | 335 |
attr | DOM 속성 | 208 |
style | 인라인 스타일 | 10 |
key | 저작용 식별자(렌더 무관) | 0 |
css | 노드 단위 스코프 CSS | 0 |
events | 이벤트 핸들러 | 0 |
ElementTag 33개 + (string & {}). 빈 문자열만 거부, 실패 시 div 대체. 실 사용 11개: div(481) span(176) button(104) img(100) input(6) li(5) p(5) 외 4개(각 1).
class — 공백 split → classList.add. attr — setAttribute, undefined/false 건너뜀, 값 문자열·불리언·유한수. text — textContent. style — Object.assign, camelCase. 인라인 style 용도: 초기 접힘(display:"none")과 진행 막대(width:"100%")뿐.
검증 규칙
src/shared/data/parsers.ts — validateUiNodeAt / validateUiDefinitionAt:
| 대상 | 규칙 |
|---|---|
| 노드 | 객체 |
| 깊이 | MAX_ |
| 순환 | WeakSet 감지 |
type | 문자열, 빈 불허 |
key | 문자열, 빈 불허 |
class | 문자열(빈 허용) 또는 문자열 배열 |
text css | 문자열, 빈 허용 |
ref | 문자열, 빈 불허 |
style | 객체, 모든 값 문자열 |
attr | 객체, 값 문자열·불리언·undefined·유한수 |
events | 객체, 값 함수 |
children | 배열, 재귀 검증 |
| 문서 | name 비어 있지 않은 문자열, ui 유효 노드, templates 값마다 검증 |
검증은 작업대가 문서를 세울 때(양식 보고) 동작. 계약 테스트(tests/data-contracts.test.ts): 중첩 통과, 빈 type 거부, 순환 거부(빌드와 테스트).
ref 계약
render() → { el, refs }. refs는 Map<string, HTMLElement>. 436개.
| 규약 | 설명 |
|---|---|
| 이름 | camelCase. 예외: tut- tut- tut- |
| 중복 | 나중 것이 덮어씀 |
| 병합 | 자식 refs → 부모로 올라옴 |
setRefText/setRefAttr | ref 없으면 조용히 지나감 |
uiTemplate(def, name) | 없는 이름 → throw |
ref 최다: villager-detail(87), settings-screen(47), seal-office-screen(44).
렌더러 · 마운트 헬퍼
렌더 단계 (src/shared/ui/dom/render.ts) | 동작 |
|---|---|
1 createElement | 실패 시 div 대체 |
2 class | classList.add |
3 style | Object. |
4 attr | setAttribute |
5 text | el. |
6 children | 재귀 + refs 병합 |
7 events | addEventListener |
8 ref | refs.set |
9 css | 스코프 스타일 주입, 래퍼 display: |
마운트 함수 (src/game/screens/ui-mount.ts) | 동작 |
|---|---|
cloneUiNode | structuredClone |
mountUiDef(def, parent?) | clone + css 렌더 |
clearAndMount(parent, def) | 부모 비우고 재마운트, 스크롤 복원 |
repeatTemplate(host, tpl, items, bind) | host 비우고 item마다 clone → bind |
uiTemplate(def, name) | 조회, 없으면 throw |
setRefText / setRefAttr | ref 있을 때만 설정 |
css 있으면 반환 el은 래퍼. 루트에 flex:1; min-height:0 필수. 리스트 렌더는 repeatTemplate+uiTemplate만 허용. 22개 문서 51개 템플릿: affinity-chart(chartRow chartChip) · blocked-dialog(blockedRow) · chat-screen(msgRow) · collect-all(resourceRow) · confirm-dialog(qtyPct) · craft(recipe material) · craft-job(material) · daily-wheel(slotLabel oddsRow) · inventory-screen(chip item) · kingdom-info(line) · kingdom-screen(menuBtn) · kingdom-sheet(row cost piece chip) · mission-screen(loginDay missionCard empty) · monster-info(statRow dropChip) · patch-notes(noteCard) · pen-detail(line) · recruit(jobCard villagerCard statChip) · recruit-detail(statChip yieldRow) · reward-popup(itemCell) · seal-office-screen(packRow packageRow passActive fundPlan fundChip shelfNote fundStepRow) · villager-detail(satchelRow craftJobRow spoilsRow equipSlot bagRow menuItem foodRow mineRow buffChip) · villagers(villagerCard empty chip).
테마 토큰
src/game/screens/theme.ts PANEL_THEME_CSS. openGameOverlay()가 화면별 CSS 앞에 주입.
| 클래스 | 핵심 값 |
|---|---|
. | position: 스크림 rgba(6,6,14,0.82) |
. | width: 세로 flex, Galmuri, #f5e6cc |
. | 세로 flex + min- |
. | flex:1 1 auto; min- |
. | 배경 #1a1a2e, 테두리 3px solid #5c4033, 그림자 inset 0 0 0 2px #8b6914 |
. | 배경 #c4a35a, 글자 #1a1a0d, 테두리 #8b6914 |
. | 배경 #5c4033, 글자 #f5e6cc, 테두리 #8b6914 |
. | 글자 #b8a088 20px |
. | #6ac86a 12px 인라인 알림 줄 |
| 용도 | 색 |
|---|---|
| 본문 / 제목 금 / 수치 금 | #f5e6cc / #e8c84a / #f5d76e |
| 주 버튼 hover / 보조 hover | #d4b36a / #6b4c3d |
| 카드 배경·테두리 / 흐린 글자 | #0d0d1a·#3a2a1a / #8a7a6a #b8a088 |
| 성공 / 경고·위험 / 인장 보라 | #6ac86a / #e8a84a #d68a8a #7a2a2a / #c9a0ff #6b4fa0 |
토스트가 두 종류인 이유
패널 안 .mlk-toast는 CSS 클래스 하나(craft mission-screen recruit villager-detail), 패널과 수명 동일. 플로팅(showToast())은 document.body z-200 호스트, 화면이 닫히는 액션의 확인용. 기준: 메시지가 패널보다 오래 살면 플로팅 — 그리고 패널 안에서도 플레이어가 보고 있지 않은 곳에 답하게 되면 플로팅입니다(영입의 금화·인장 부족). 플로팅은 pointer-events: none — 화면 위에 떠 있으므로 탭을 먹지 않고 뒤쪽 버튼/탭으로 흘려보냅니다.
UI 문서 37개
| 파일 | 화면 | 루트 class | 노드 | ref | tpl | 폭 |
|---|---|---|---|---|---|---|
bottom- | 하단 탭바 | sdv-bar | 22 | 3 | 0 | — |
villagers | 주민 탭 | v- | 33 | 22 | 3 | 720px |
inventory- | 가방 탭 | sdv- | 64 | 22 | 2 | 420px |
kingdom- | 왕국 탭 | kd- | 24 | 15 | 1 | 500px |
mission- | 미션 탭 | mi- | 68 | 44 | 3 | 500px |
seal- | 인장소 탭 | sh- | 193 | 59 | 7 | 480px |
settings- | 설정 탭 | s- | 82 | 52 | 0 | 420px |
chat-fab | 채팅 버튼 | mlk- | 4 | 3 | 0 | — |
chat- | 반투명 채팅창 | mlk- | 20 | 13 | 1 | — |
villager- | 주민 상세 | mlk- | 186 | 124 | 9 | — |
monster- | 적 정보 | sdv- | 27 | 16 | 2 | 360px |
affinity- | 속성 상성표 | sdv- | 24 | 11 | 2 | 360px |
recruit | 영입 | mlk- | 48 | 25 | 3 | — |
collect-all | 일괄 수거 | mlk- | 23 | 15 | 1 | — |
craft | 제작 | mlk- | 53 | 39 | 2 | — |
craft-job | 제작 중 항목 | mlk- | 24 | 14 | 1 | — |
confirm- | 확인 대화상자 | sdv- | 28 | 17 | 1 | 360px |
recruit- | 후보 상세 | sdv- | 38 | 22 | 2 | 380px |
shop- | 상품 설명 | sdv- | 17 | 10 | 0 | 360px |
kingdom- | 왕국 하위 시트 5종 | ks- | 49 | 31 | 4 | — |
kingdom- | 건축·행사 상세 | sdv- | 26 | 16 | 1 | 360px |
pen- | 우리 상세 | sdv- | 28 | 17 | 1 | 380px |
daily- | 행운의 돌림판 | dw- | 24 | 13 | 2 | 360px |
patch- | 패치 노트 | mlk- | 17 | 9 | 1 | — |
patch- | 패치 노트 상세 | mlk- | 15 | 9 | 0 | — |
feedback- | 피드백 | mlk- | 18 | 11 | 0 | — |
blocked- | 차단 해제 | mlk- | 16 | 9 | 1 | — |
reward- | 획득 팝업 | mlk- | 35 | 14 | 1 | — |
seal- | 인장 교환 | sdv- | 15 | 6 | 0 | 360px |
intro- | 인트로 셸 | mlk- | 16 | 14 | 0 | — |
name- | 이름 입력 | name- | 10 | 7 | 0 | min(400px,88%) |
tutorial- | 코치 카드 | tut- | 6 | 4 | 0 | — |
hud | 미사용 | sdv-hud | 10 | 0 | 0 | — |
status- | 미사용 | sdv- | 11 | 0 | 0 | 360px |
daily- | 미사용 | dl- | 45 | 0 | 0 | 480px |
action- | 미사용 | sdv- | 26 | 0 | 0 | 320px |
dialog- | 미사용 | sdv- | 7 | 0 | 0 | 480px |
합계 1,352 노드, 686 ref, 51 템플릿, 8,119줄. 32개 static import, 5개 미참조.
새 화면 추가 절차
ui/<이름>.ui.json만들기 (작업대의 UI 탭에서 새 파일 → 연필로 원문 편집).- 루트 클래스. 오버레이:
mlk-panel mlk-shell, 스크롤:mlk-scroll. - CSS. 색은 위 팔레트, 여백
var(--mlk-screen-pad). - TS 참조 노드에만
ref(camelCase). - 반복 행 →
templates. 루트에 프리뷰 샘플만. src/game/screens/overlays/에 TS, 문서 static import.openGameOverlay→clearAndMount→ refs 바인딩.setRefText(refs, name, t("…")).- 아이콘 →
public/ui/icons/, 문서에/ui/icons/…. game.html?ui로 그림 확인 →pnpm typecheck→pnpm test→pnpm build.
탭 화면: 루트 flex:1; min-height:0 필수. SCREEN_KEYS 키, SCREEN_COLORS 색, bottom-tab-bar.ui.json에 data-screen 버튼, main.ts 등록. styleId=mlk-<이름>-css, singletonKey 필수. 열쇠 규칙 다국어 텍스트, 셸 규약 화면 구성, 게이트 빌드와 테스트.
떠 있는 버튼(ui/chat-fab.ui.json): 바닥 기준을 화면과 똑같이 max(var(--mlk-bar-h), var(--kbi, 0px)) 로 잡으세요. 그러면 소프트 키보드가 올라올 때 버튼도 같이 올라갑니다. z-index 는 탭바(100)와 오버레이(150) 사이. 겹쳐 뜨는 대가로 스크롤 목록의 오른쪽 끝 버튼을 가릴 수 있다는 점은 알고 있는 트레이드오프입니다.
이어서 읽기
- 화면 구성 — 오버레이 생명주기, 마운트 시점
- 데이터 파일 규격 — 나머지 JSON 스키마
- 다국어 텍스트 — 자리표시자를 덮는 방법
- 소리와 에셋 파이프라인 — 아이콘 생성
- 개발 도구 — UI 탭