내 작은 왕국 기획 위키

화면 구성

01 게임

이 문서 안에서

플레이어가 보는 화면의 뼈대를 정합니다. 화면 안의 동작은 코어 루프와 각 시스템 문서가 다루고, 여기서는 틀만 다룹니다. ui/*.ui.json 문법은 UI 선언 시스템.

한 줄 요약 — 화면 전체가 DOM 탭 5개와 모달 오버레이이고, Phaser 는 왕국 광장 씬 하나와 작은 무대 3종(224×120 비네트 2종 + 240×160 가축 우리)에만 들어갑니다.

1. 월드 씬은 왕국 광장 하나뿐

src/game/main.ts에 Phaser import 없음. Phaser 가 들어가는 곳은 넷입니다.

무엇크기·설정
왕국 광장고정 월드(data/plaza.ts), zoom 2 고정, Scale.RESIZE, 카메라 드래그
주민 상세 작업 비네트224×120, input:false
전사 전투 비네트224×120, input:false
가축 우리 상세240×160, Scale.FIT, input:false

전부 pixelArt:true, physics 없음, audio:{noAudio:true} 이고 동적 import 로 붙습니다. 청크가 실패해도 상세 화면과 왕국 허브는 그대로 동작합니다 — 광장 자리만 빕니다. 렌더러가 지연 청크로 남아 있는지는 tests/architecture.test.ts 가 지킵니다.

왕국 광장이 씬인 이유는 크기입니다. DOM display:grid 로 그리던 동안 광장의 크기는 기기에 남은 자리였고, 같은 왕국이 짧은 폰에서는 납작한 띠, 태블릿에서는 긴 밭이었습니다. 지금은 월드 픽셀로 authored 된 고정 크기를 고정 배율로 그리고 화면이 그 일부를 보여 줍니다 — 큰 화면은 더 큰 왕국이 아니라 왕국의 더 많은 부분을 봅니다. 스크롤이 가능해졌으므로 광장이 화면보다 커질 수 있고, 그래서 유료 확장이 가능해졌습니다. 폭은 8열 × 36 + 여백 8 × 2 = 304 월드 픽셀(zoom 2 에서 608 CSS 픽셀)로, 어떤 기기에서도 화면보다 넓습니다 — 그래서 카메라는 두 축 모두 실제로 움직입니다. 4열(=320 CSS 픽셀)이던 동안 가로 드래그는 어디서도 아무 일을 하지 않았습니다: 뷰포트보다 좁은 월드는 clampCameraScroll 이 가운데로 붙들어 손가락이 얼마를 움직여도 같은 x 를 돌려주기 때문입니다. 카메라가 처음 열리는 자리는 initialCameraScrollX/Y — 세로는 첫 칸 줄, 가로는 있는 칸들의 가운데입니다(칸 두 개짜리 새 왕국을 월드 중앙에 맞추면 폰 화면에서 두 칸이 왼쪽 밖으로 밀려납니다). 비네트 역할 11종(VIGNETTE_ROLES, 영입 가능한 직업 전부)은 아트와 LPC 캐릭터 참조.

광장 위쪽에는 왕궁이 늘 서 있습니다. 세이브의 건물 행이 아니라 항상 존재하는 하나이고, 짓지도 옮기지도 허물지도 못합니다 — 이 게임이 다시 세우고 있는 대상 자체이기 때문입니다. 자리는 palaceRect()(월드 폭 가운데, 첫 칸 줄에 밑변을 대고 64×80), 외형 5종은 PALACE_TIERS 이고 왕의 인장 0/8/14/20/26으로 영구 해금합니다. 왕국 레벨은 외형을 더 이상 자동 변경하지 않으며, 산 외형은 언제든 무료로 다시 고릅니다. 각 외형의 스프라이트(assets/sprites/palace/<id>.png, PALACE_TIERS.art 가 가리킴)를 씬이 텍스처로 바꿉니다 — AI 그림을 매지ента 크로마키로 투명화해 scripts/generate_palace_art.py 가 만든 64×80 픽셀아트입니다. 누르면 왕궁 시트가 열려 외형 전체와 가격을 보여 주고, 구매 확인을 거친 뒤 즉시 입힙니다. 푸터의 버튼은 왕실 꾸미기 시트로 들어갑니다(허브의 왕실 문과 같은 시트, 같은 레벨 게이트 chamberLevelReq()).

치장 테마는 이 세 가지 겉모습을 한 벌로 갈아입힙니다 — 광장의 하늘·풀·마당 색, 왕궁의 외형 5단계 그림, 왕실의 벽·바닥 마감입니다. 문은 허브 격자가 아니라 광장 위 한 줄의 왼쪽입니다(오른쪽은 광장 확장 버튼). 그 줄은 "광장 3/9칸"을 적던 자리였고, 그 숫자는 없앴습니다 — 빈 칸은 광장이 이미 그려서 보여 주고, 말로 적힌 숫자가 뭔가를 결정하는 곳은 확장 창 안뿐입니다. 버튼은 입고 있는 테마의 미리보기 타일을 달고 있어서, 라벨이 아니라 왕국의 현재 모습이 아이콘입니다. 격자에 남은 네 문은 전부 기다리는 일이고 각각 숫자 배지를 답니다 — 옷장은 기다리지 않으므로 배지가 켜질 수 없는 유일한 문이었습니다.

치장은 왕의 인장으로 삽니다(테마당 24, 확인 대화상자를 거쳐 시트 안에서 구매 — 2026-08-10까지는 IAP 상품 2종이었습니다). 사면 그 자리에서 입습니다: 방금 산 겉모습을 보러 한 번 더 누르게 하는 것은 시트가 할 일이 아닙니다. 산 것은 kingdom.themes(옷장)에, 입은 것은 kingdom.theme에 남고, 인장이 모자라면 부족분을 덮는 오퍼가 열립니다(인장소와 상품). 경계가 하나 있고 그것이 이 기능의 전부입니다: 테마는 왕궁 외형의 소유권이나 선택을 바꾸지 못합니다. 외형은 별도 선반에서 사고, 테마는 선택된 외형을 무엇으로 그리는지만 바꿉니다 — 그래서 테마마다 5장이 전부 있어야 합니다(한 장짜리 테마는 서로 다른 값으로 산 외형들을 같은 그림으로 만들어 버립니다). 왕실 마감은 레벨 사다리를 덮지 않고 늘립니다 — 골드로 산 마감은 테마를 갈아입어도 그대로 있고, 테마와 함께 온 마감만 테마를 벗을 때 사다리 기본값으로 돌아갑니다. 벽·바닥 칩도 누르는 즉시 바뀌지 않고 확인 대화상자에서 위치·이름·가격을 확인한 뒤 적용됩니다.

광장 아래쪽에는 가축 마당이 붙습니다. 칸 줄과 그 아래 여백(다음 줄이 될 자리)을 지나서 시작하는 띠이고, 우리 한 곳이 48×36 자리 하나를 씁니다(한 줄에 6곳, penRect() — 6×48 이 칸 8열의 8×36 과 정확히 같은 폭이라 마당은 칸 줄의 여백을 그대로 빌려 씁니다). 마당은 광장 칸과 똑같은 방식으로 그립니다: 지은 우리 수가 아니라 왕국이 열어 둔 자리 수만큼 땅과 울타리를 미리 깔고, 우리를 지으면 그 자리에 오두막이 들어섭니다(yardSlots). 빈 자리의 울타리는 흐리게, 우리가 선 자리는 진하게 그려서 — 빈 칸 윤곽선과 건물의 관계와 같습니다 — 아직 남은 자리가 어디인지 시트를 열지 않고도 보입니다. 자리 수는 왕국 레벨 사다리에 더해 유료 광장 확장 1단계마다 2곳씩 늘어납니다(penCapacity, PEN_SLOTS_PER_EXPANSION). 광장 칸과 우리 자리는 둘 다 언제나 짝수입니다 — 레벨 사다리도 두 칸씩, 유료 확장도 두 칸씩 올라가므로 한 줄 끝에 홀로 남는 자리가 생기지 않습니다(PLAZA_CELLS_BY_LEVEL, PEN_SLOTS_BY_LEVEL, tests/kingdom.test.ts). 마당은 칸과 똑같이 레벨 1부터 깔려 있습니다 — 가축 문이 레벨 2에 열리는 것은 문에 관한 사실이고 땅에 관한 사실이 아니기 때문입니다(yardSlots). 레벨로 마당을 가려 두었을 때는, 광장 확장 시트가 이미 "가축 마당도 2곳 넓어집니다"라며 자리를 세어(penCapacity) 인장을 받고는 그 땅을 그리지 않았습니다. 우리를 하나도 짓지 않았어도, 아직 아무 우리도 지을 수 없는 레벨이어도 마당은 이미 거기 있습니다. 자리마다 오두막 스프라이트와 울타리를 그리고, 산 마리 수만큼 동물이 그 안을 오갑니다 — 닭은 최대 3마리, 소는 1마리입니다(AnimalArt.yardMax: 48픽셀 자리에서 소 두 마리는 소 모양 얼룩 하나입니다). 기다리는 산물이 있으면 오두막 위에 개수 배지가 붙고 — 가득 차면 초록에서 호박색으로 바뀝니다. 우리는 누르면 열리고 꾹 누르면 들려서 다른 자리로 옮겨집니다 — 빈 자리에 놓으면 이동(movePen), 우리가 선 자리에 놓으면 자리 교환(swapPens)이고, 이는 건물의 moveBuilding·swapBuildings와 같은 한 쌍입니다. 마리 수와 저장 시각은 우리와 함께 따라갑니다(들고 다니는 것으로 산물을 거둘 수도, 잃을 수도 없습니다). 우리가 서 있는 자리는 LivestockPen.slot에 적히고, 이 필드가 없던 세이브는 배열 순서로 읽습니다(penPlacements) — 처음 만지는 동작이 전부를 기록합니다. 우리를 헐면 그 자리는 빈 자리로 남고 옆 우리는 움직이지 않으며, 다음에 짓는 우리가 그 빈 자리에 들어섭니다. 그냥 끌면 카메라가 움직입니다. 열리는 것은 ui/pen-detail.ui.json 대화상자이고, 그 안의 240×160 무대(scenes/pen-scene.ts)에서 세이브에 적힌 수 그대로의 동물이 돌아다닙니다. 배경과 오두막은 임시 그림이고 닭·소는 LPC 아트입니다(아트와 LPC 캐릭터). 양 우리의 양은 아직 없어서, 그 우리는 오두막과 배지만 그리고 안은 비워 둡니다 — 남의 동물을 대신 세우지 않습니다.

포인터 하나가 여러 일을 하고, 무엇인지는 누름 시점의 히트테스트와 누른 시간으로 정합니다.

누른 곳꾹 누르기(320ms)끌기
건물상세 대화상자들어서 운반 — 다른 칸에 놓으면 이동/교환카메라
가축 우리우리 무대들어서 운반 — 빈 자리는 이동(movePen), 찬 자리는 교환(swapPens)카메라
주민이름표즉시 들어 올려 옮기기
왕궁왕궁 오버레이카메라
그 밖카메라

건물과 우리에 시간 조건이 붙는 이유는 칸이 광장의 대부분을 덮기 때문입니다. "건물에서 시작한 끌기"는 왕국을 움직이겠다는 뜻이어야 하고, 두 뜻을 가르는 신호는 시간밖에 없습니다 — 아이폰 홈 화면이 하는 것과 같은 거래이고, 흔들림도 같은 답입니다: 꾹 누르기가 먹혔는지, 그리고 그것이 무엇에 대한 것인지(건물을 들면 건물들이, 우리를 들면 우리들이 흔들립니다)를 말 없이 알려 줍니다. 흔들림은 트윈이 아니라 update()에서 매 프레임 살아 있는 스프라이트를 다시 읽어 그립니다 — 화면이 1초마다 씬을 다시 싱크하므로 누름 시점에 잡아 둔 트윈 목록은 새로 생긴 스프라이트를 빠뜨리고 사라진 스프라이트를 붙들고 있게 됩니다.

주민만 꾹 누르기가 없습니다. 주민 아래에는 풀밭뿐이어서 끌기가 다른 뜻일 수 없고, 유일하게 가능한 일을 하려고 기다리게 하는 것은 기다림에 값을 매기는 일입니다. 내려놓은 자리는 세이브에 적지 않습니다 — 주민의 레인은 원래 id 해시(laneOf)에서 나오므로, 이 이동은 씬이 살아 있는 동안의 연출이고 다음 방문에는 해시가 정한 자리에서 다시 걷습니다. "내 사람들이 서 있는 자리" 필드는 세이브 마이그레이션과 클라우드 충돌을 불러오는데, 플레이어가 심심풀이로 옮기는 것에는 과한 값입니다.

카메라를 겨눌 때 Phaser 의 scrollX/scrollY 를 화면 왼쪽 위의 월드 좌표로 쓰면 안 됩니다. Phaser 는 카메라 중심을 기준으로 zoom 을 적용하므로 보이는 띠는 카메라 크기 / zoom 이고 그 중심이 scroll + 카메라 크기 / 2 입니다. 그래서 씬은 뷰 원점으로만 말하고 cameraScrollFor() / cameraOriginFor() 에서만 렌더러의 규약을 다룹니다. 이 둘을 혼동하던 동안 360px 캔버스가 월드 x 0 이 아니라 80 에서 열려 칸 절반이 화면 밖에 있었고 드래그 클램프도 엉뚱한 수를 붙들고 있었습니다(tests/plaza-world.test.ts).

2. 화면 상자 (game.html)

항목
lang 속성없음 — 로케일 감지 간섭 방지 (다국어 텍스트)
뷰포트width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover, maximum-scale=1.0
기본 CSSmargin:0; padding:0; background:#000; height:100%; overflow:hidden, #game-container { position:fixed; inset:0 }
가로모드 차단@media (orientation:landscape) and (max-height:500px) → 한국어 하드코딩 안내

3. 층 구조

z-index무엇붙는 곳
10탭 화면 6개#game-container
100탭바 .sdv-bar / 세이브 실패 배너#game-container
150모달 오버레이 .mlk-overlaydocument.body
200토스트 .mlk-toast-hostdocument.body
400제작·레벨업 연출document.body
450골드 연출 .mlk-goldfxdocument.body
9000인트로 컷씬 .mlk-intro#game-container
9999튜토리얼 .tut-overlay / 가로모드 경고컨테이너 / body

4. 6개 탭

buildGame()이 6개 div를 한꺼번에 만들어 visibility로만 전환합니다. 탭을 누를 때마다 빌더 재실행.

DOM id배경색정의
주민villagersscreen-villagers#1a1a2eui/villagers.ui.json
가방inventoryscreen-inv#2d1a2eui/inventory-screen.ui.json
왕국kingdomscreen-kingdom#1a232eui/kingdom-screen.ui.json
미션missionscreen-mission#1a2e2eui/mission-screen.ui.json
인장소shopscreen-shop#2e2a1aui/seal-office-screen.ui.json
설정settingsscreen-settings#2e1a1aui/settings-screen.ui.json

화면 위치: position:fixed; inset: var(--sat) 0 max(var(--mlk-bar-h), var(--kbi, 0px)) 0. 채팅 버튼은 이 화면들 위에 겹쳐 뜹니다(자리를 비워 주지 않습니다). 대신 버튼이 켜져 있는 동안 --mlk-fab-clear(42px, 꺼지면 0px)를 발행하므로, 코너에 자기 조작 버튼이 있는 화면은 자기 스타일시트에서 그만큼 비킵니다 — 주민 .v-footerpadding-right(그만큼 모두 수거 가 줄고 새 주민 영입 이 왼쪽으로 밀립니다), 인장소 .sh-restorewidth: calc(100% - …). 버튼 발자국(46+10=56px)보다 작은 이유는 패널이 이미 화면 가장자리에서 들어와 있기 때문입니다 — 공용 프레임이 화면 패딩 8px + 테두리 3px + 패널 패딩 8px 를 쓰므로 패널 위로 겹치는 건 37px 뿐입니다. 여기에 간격 5px 을 더합니다(37+5=42) — 양쪽 다 두꺼운 픽셀 테두리(버튼 3px, 패널 버튼 2px)를 두르고 있어서 박스가 조금만 떨어져도 프레임이 갈라져 보이고, 화면 가장자리와 같은 10px 은 구멍처럼 보였습니다. 56px 를 다 넘기면 필요보다 20px 더 밀립니다. --sar 는 양쪽에 똑같이 붙어 상쇄되므로 식에 없습니다. 남는 대가는 스크롤되는 행의 오른쪽 끝 버튼(인장소 구매)이고, 스크롤을 조금 움직이면 닿습니다.

탭바 DOM 순서: 주민→가방→왕국→미션→인장소→설정 (코드 생성 순서와 다름, data-screen으로 매칭).

탭 전환 시 살아남는 상태저장 위치
주민 역할 필터모듈 전역 (villager-filter.ts)
가방 카테고리 필터모듈 전역 (inventory-filter.ts)
미션 하위 탭dataset.missionTab
스크롤 위치clearAndMount가 복원

채팅은 탭이 아닙니다. 우측 하단 플로팅 버튼(ui/chat-fab.ui.json)이 반투명 채팅창을 openGameOverlay로 띄웁니다. 컨트롤러는 모듈 전역 싱글턴(소켓·로그·IME 유지)이고 창이 닫혀 있어도 계속 수신하므로, 닫힌 동안 들어온 줄은 버튼의 미읽음 배지로 셉니다. soloMode 시 버튼 숨김 + 컨트롤러 파괴, applyConnectivity가 오프라인 시 자동 전환.

핵심상세
주민카드 목록·드래그 정렬·모두 수거·영입주민과 직업, 명부와 영입
가방5열 격자·판매(수량 스테퍼)인벤토리와 판매
왕국껍데기만 — 성 문장과 kingdom.soon 한 줄
미션5 하위 탭(로그인·일일·주간·월간·업적)미션과 출석
인장소한정 특가→패키지→재건 기금→시즌 패스→인장팩→영구 특권→골드 교환인장소와 상품
설정패치 노트→튜토리얼→BGM/SFX→언어→클라우드→싱글→UID→차단→피드백소리와 에셋 파이프라인
채팅(창)로비, 뮤트·신고, 미읽음 배지실시간 방과 채팅

탭바: ui/bottom-tab-bar.ui.json, 탭 6개, height:52px, border-top:3px solid #8b6914. 라벨은 한 줄 고정(white-space: nowrap, font-size: clamp(9px, 2.5vw, 11px), 영어는 clamp(8px, 2.3vw, 10px)) — 탭이 6개가 되면서 360px 폭에서 한 탭이 60px 밖에 안 되고, 그대로 두면 영어 Seal Office 와 일본어 ミッション 이 두 줄로 접혀 52px 상자를 넘겼습니다. 320px 영어 Seal Office 만 끝이 잘립니다(이전에는 같은 폭에서 두 줄로 접혔습니다). --mlk-bar-inset: clamp(6px, var(--sab, 0px), 10px), --mlk-bar-h = calc(55px + var(--mlk-bar-inset)).

채팅 버튼: ui/chat-fab.ui.json, 46×46, right: calc(10px + var(--sar)), bottom: calc(max(var(--mlk-bar-h), var(--kbi)) + 10px), z-index:120(탭바 100 < 120 < 오버레이 150). 화면과 같은 바닥 기준(max(탭바, 키보드))을 쓰므로 IME 가 올라오면 버튼도 같이 올라갑니다 — tests/tab-bar-inset.test.ts 가 지킵니다.

소프트 키보드

두 갈래이고, 한 갈래가 조용히 죽어 있었습니다.

환경누가 올리나
브라우저·iOS키보드가 전체 높이 뷰포트를 덮고 visualViewport 만 줄어듭니다. --kbi 가 그 높이를 CSS 로 옮깁니다
Android 셸MainActivity 가 WebView 를 IME 만큼 마진합니다. 뷰포트 자체가 줄어드니 --kbi 는 0 이 맞습니다

키보드가 올라온 동안에는 <html>.kb-open 이 붙고 탭바가 접힙니다(display:none + --mlk-bar-h: 0px). 입력 중에는 이동할 곳이 없으니 탭바의 55px 을 메시지 로그에 줍니다. 판정은 포커스가 아니라 뷰포트입니다(isKeyboardOpen): 브라우저·iOS 는 --kbi > 0, Android 는 "키보드 없을 때 최대 높이보다 120px 이상 줄었는가". 포커스로 판정하면 Android 뒤로가기로 키보드를 닫았을 때 포커스가 입력창에 남아 탭바가 사라진 채로 남습니다.

targetSdk 35 는 창이 항상 edge-to-edge 라서 매니페스트의 adjustResize 는 혼자서는 아무 일도 하지 않습니다. 키보드는 WindowInsetsCompat.Type.ime() 로 오고 앱이 적용해야 합니다. insetWebViewFromSystemBars()systemBars|displayCutout 만 요청하고 CONSUMED 를 반환하던 동안 IME inset 은 버려졌고, WebView 높이가 그대로라 visualViewport 도 줄지 않아 --kbi 가 0 에 머물렀습니다 — 채팅 입력창이 키보드 뒤에 남았습니다.

가로 회전 안내(@media (orientation: landscape) and (max-height: 500px))에 min-width: 500px 가 붙어 있는 이유도 여기입니다. 키보드가 올라온 세로 화면은 411×400 — 높이보다 넓어서 orientation: landscape 에 걸립니다.

5. 화면 프레임 규약

fillScreen(el, key): display:flex; overflow:hidden. 스크롤은 .mlk-scroll 전담. CSS 변수: --mlk-screen-pad-y:6px, --mlk-screen-pad-x:8px, --mlk-panel-pad:8px.

6. 오버레이 규약

항목규칙
생성openGameOverlay(opts)document.body, .mlk-overlay, role="dialog"
닫기close() / Escape / 백드롭 탭
효과음열기 ui.open, 닫기 ui.close
스크림rgba(6,6,14,0.82)
패딩16px+var(--sat), 아래 16px+var(--sab), 좌우 8px+var(--sal/sar)
singletonKey같은 키 이전 오버레이를 먼저 닫음
Esc맨 위 한 장만 닫음 (스택)
markBusy()400ms 중복 클릭 방지
오버레이singletonKey최대 폭
주민 상세villager-detail460px
영입recruit500px
일괄 수거collect-all420px
제작craft480px
제작 중 항목craft-job380px
후보 상세recruit-detail380px
확인 대화상자confirm-dialog380px
상품 설명shop-info360px
패치 노트patch-notes480px
피드백feedback-dialog420px
차단 목록blocked-dialog420px

7. 토스트

수치
유지2,600 ms
페이드300 ms
동시 최대3 (스택)
위치calc(var(--sat) + 56px)
최대 폭min(420px, 100%)
info 금색 #e8c84a/#8b6914/#2a1a0d, warn 주황 #e8a84a/#8a4a4a/#2e1414
상호작용없음 — 호스트와 본체 모두 pointer-events: none

토스트는 살아 있는 화면 위에 떠 있으므로 탭 입력을 가로채지 않습니다. 헤더 줄이나 탭바를 겹쳐도 탭은 뒤쪽 버튼/탭에 그대로 전달되고, 토스트는 자기 타이머로만 사라집니다(눌러서 닫기 없음).

쿠폰 동기화 완료가 인트로 중이면 gamePainted 후에 표시합니다.

8. 안전영역과 소프트 키보드

변수
--sat/--sab/--sal/--sar네 방향 안전영역 (env(), 안드로이드는 네이티브)
--kbi소프트 키보드 높이
--mlk-bar-insetclamp(6px, var(--sab, 0px), 10px)
--mlk-bar-hcalc(55px + var(--mlk-bar-inset))

--kbi 사용: 탭 화면 inset, 인트로 이름 입력 모달, 피드백 오버레이.

9. 전체화면을 덮는 세 가지

표면z-index조건
인트로 컷씬9000첫 실행 (세계관과 인트로)
튜토리얼9999첫 실행 1회 (tutorialSeen) · 설정에서 다시 보기 · ?tutorial=demo 강제
세이브 실패 배너100읽기 실패 시 상단 띠 (세이브와 마이그레이션)

10. ui/*.ui.json 마운트

게임 코드가 import하는 20개:

파일화면
bottom-tab-bar탭바
villagers / inventory-screen / kingdom-screen / mission-screen / seal-office-screen / settings-screen탭 6종
chat-fab / chat-screen채팅 버튼 + 반투명 채팅창
villager-detail / recruit / recruit-detail / collect-all / craft / confirm-dialog / shop-info / patch-notes / feedback-dialog / blocked-dialog오버레이 10종
intro-root / name-input인트로
tutorial-coach튜토리얼

참조 없는 5개(hud, status-bars, daily-login, action-menu, dialog-sample)는 남겨둡니다. 5개 정의는 스토어 이미지 렌더러가 재사용합니다(마케팅 소재).

이어서 읽기

이 문서를 가리키는 곳