Skip to content

모달 (Modal) ​

모달 3종의 규격과 공통 동작을 확인하고, 어느 것을 쓸지 고르는 기준을 찾아보세요.

패턴 · Web · 1차 표준(v1) · 문서 포맷 시범 적용

실제 design-system.css가 그대로 적용된 데모입니다. 3종 모두 아래 탭에서 만져 볼 수 있습니다 — 각 탭 맨 위가 그 종류의 라이브 예시입니다.

규칙 (강제) · 코딩 에이전트용

⚠️ 이 문서는 v1(진행 중, 확정 아님) — 딤 40%·간격·타이포 수치·닫기 동작은 조정될 수 있으니 수치를 하드코딩하지 말고 이 문서를 정본으로 참조. 급여(hr-system) 포함 전 앱 적용 대상(2026-07-29 제외 해제) — 단 레거시는 위반이 아니라 마이그레이션 대상이니, 이미 작업 중인 파일 안에서 교체하세요.

  • 🚫 네이티브 시스템 다이얼로그 금지 (예외 없음): alert() / confirm() / prompt() 를 새로 쓰지 마세요. 2026-07-29에 전 앱 244곳을 걷어냈고 현재 제품에 0곳입니다. 대신 askConfirm / showAlert / showError / showPrompt(@hereby/components), HTML은 $store.dialog.ask(...) → 확인·알림·입력 다이얼로그. window.alert · (window as any).confirm · window["prompt"] 같은 우회 표기도 전부 금지 — 캐스트형이 실제로 9곳 새어 나간 전례가 있습니다. 이중으로 막혀 있습니다: Biome lint/suspicious/noAlert(error) + pnpm check:dialogs(pre-commit blocking, 캐스트형·.html 담당).
  • 🚫 신규 기능의 모달은 무조건 이 3종을 따른다(→ 새 모달을 만들 때). 백드롭 직접 조합(fixed inset-0 bg-…)·bg-black/*·카드에 직접 준 패딩·새 .ds-mini-modal·제목에 얹은 색은 커밋이 막힌다(pnpm check:modals, pre-commit blocking, 추가된 줄만 검사). 정당한 예외는 ds-modal-exempt: <사유> 주석으로만 통과.
  • 모달은 3종뿐(→ 모달은 3종입니다): 컨펌 다이얼로그(.ds-dialog) · 일반 모달(.ds-modal) · 패널 모달(.ds-modal--panel). 네 번째 종류를 만들지 말 것. 셋은 백드롭·닫기·스크롤잠금·카드 내부 구조가 전부 같고 바깥 여백과 크기만 다르다.
  • 컨펌 다이얼로그 여백은 상하좌우 28 일괄(일반 모달 44/40/36과 다름). 마크업에 padding 을 적지 말고 카드에 .ds-dialog 만 얹을 것 — 수치는 design-system.css 의 .ds-dialog .ds-modal__* 가 갖는다. 안쪽 간격(타이틀↔본문 24 · 본문↔푸터 32)과 타이포는 일반 모달과 동일.
  • 컨펌 다이얼로그 타이틀은 on/off, 기본값은 「확인」(종류 불문 — alert/confirm/prompt 모두). 끄려면 title: "" 또는 title: false(헤더가 통째로 빠지고 .ds-dialog--no-title 이 본문 위 여백을 대신 진다). 종류별로 다른 기본 제목(「안내」/「오류」/「입력」)을 되살리지 말 것 — 실패·파괴 신호는 제목이 아니라 푸터 버튼이 진다(tone: "danger" 는 확인 버튼 색만 바꾼다).
  • 핵심 3원칙: dim 항상 보임 · 바깥 클릭으로 안 닫힘 · 뒤 화면 스크롤 잠금.
  • 백드롭: 반드시 .ds-modal-backdrop 하나로(fixed inset-0 + dim 40% + flex 중앙 + z토큰 + 스크롤 잠금). ❌ fixed inset-0 bg-... 직접 조합, ❌ bg-black/40(투명 버그), ❌ bg-[var(--ds-dim)](45%) 금지. 모달 위 모달 --top(z70), 전역 팝업 --system(z75), 임베드(ga-embedded/tm-embedded)는 padding-bottom:10vh.
  • 이름 함정: .ds-modal(접미사 없음)은 카드, 백드롭은 .ds-modal-backdrop. 카드 셸 .ds-modal bg-white w-full mx-4 max-h-[90vh] flex flex-col relative + 인라인 max-width.
  • 화이트 딤 --white는 처리 중(로딩) 전용(→ 화이트 딤): 딤 색만 --ds-glass-70으로 바뀌고 나머지는 기본 백드롭과 같다. 사용자 확인을 받는 다이얼로그에는 쓰지 않는다(그건 어두운 딤) — 흰 딤 위에 선택 버튼을 얹지 말 것. 스피너 + 한 줄 안내까지가 이 딤의 내용물이다.
  • 카드 폭은 5단 스케일에서만 고른다 — 320 / 580 / 780 / 980 / 1180px. 중간값(460·640·900 …)을 새로 만들지 마세요. 내용이 넘치면 다음 단으로 올리고, 그래도 안 되면 패널 모달입니다. 미니 팝업(.ds-mini-modal)은 320 고정(w-80). 상세: 카드 폭 스케일.
  • 미니 팝업(.ds-mini-modal) 내부도 큰 모달과 같은 규격: 제목은 ds-modal__title(20/600, 14px 자체 값 금지) · 제목↔본문 24px(제목+본문을 flex flex-col gap-6로 묶어라 — 카드의 gap:32px는 그 묶음과 버튼 줄 사이에만 걸린다) · 제목 바로 아래가 설명 문장이면 ds-modal__desc(14/400, 8px) · 폭 320(w-80). X(ds-modal__close)는 입력 폼형에만 달고(사본 만들기·이름 바꾸기), 확인/알림형은 취소·확인 버튼으로 충분해 달지 않는다. 카드에 position:relative가 이미 있어 X는 클래스만 얹으면 제자리에 붙는다.
  • 닫기: 변경 없는 폼·뷰어·확인은 X/취소/Esc → 바로 닫힘, 변경 있는 폼은 저장확인 경유(attemptClose). 새 폼 모달은 closeGuardForms에 등록 필수(미등록=항상 확인, 안전 기본).
  • 스크롤 잠금: 백드롭 내장(overscroll-behavior:none + 오버플로 0). ❌ contain+강제 1px 오버플로로 "고치기" 금지(회귀).
  • 타이포·간격(일반 모달 기준 — 컨펌 다이얼로그는 위 항목의 28): 카드 마진 44/40/36 · 타이틀 20/600(ds-modal__title) · 디스크립션 14/400 · 그룹 타이틀 16(text-b3)/500 · 필드 타이틀 13(text-b5)/500 · 타이틀↔인풋 6px(mb-1.5) · 그룹 세로 16px(space-y-4) · 좌우 인풋 12px(gap-3). 모달 안 탭 바는 tabnav-flush-left.
  • 텍스트 색: raw neutral-* 대신 label-* 토큰 — 타이틀·그룹 타이틀 label-normal, 디스크립션·인풋 타이틀 label-assistive, 보조 label-muted.
  • 상태 선택: pill·스위치 대신 불리언 select(ds-select ds-select--32 w-full + <option value="true/false">). ds-switch는 기능 on/off에만.
  • 푸터 버튼(weight 3티어): 위치 아닌 중요도로 — 기본 컨펌 ds-btn--dark > 크리티컬(삭제) ds-btn--lightor(남발 금지) > 물러남(취소·닫기) ds-btn--white-bk. 위험 반전: 저장확인 계속편집=dark/닫기=white-bk, 삭제확인 삭제=lightor·기본 포커스는 취소. 예외: 워크플로 승인=green·반려=red 의미색 유지.
  • z 토큰: dim --ds-dim-40, 모달 --ds-z-modal(50), 모달위모달 60, --top 70, --system 75(전역 팝업은 항상 system).
  • 피커(모달 안): 다이얼로그형(시간)=--top으로 위에(+@keydown.escape.stop으로 모달 escape 차단), 팝오버형(월 .ds-menu)=바디 앵커 절대배치 비잘림. Esc/바깥클릭은 피커만 닫음.
  • 패널 모달 .ds-modal--panel: width:min(100vw-200px,1280px)·min-width:768px·height:100vh-120px·min-height:300px(변형 -loose/-narrow/-tall). 좌측 메뉴 220px bg-neutral-50. 중첩 자식 ESC 가드(typeManagerChildOpen·stopImmediatePropagation·500ms 유예).
  • 모바일(<768) 폼 모달 — 기본값 권장, 강제 아님: 풀페이지(.ds-modal--mobile-full) + 폼 컨트롤 ds-input--48/ds-select--48 + CTA ds-btn--lg 풀폭이 출발점입니다(사이즈는 픽셀이 아니라 사이즈 클래스로 지정 — 인풋은 숫자 표기, 버튼은 영문 표기). 화면이 요구하면 스케일 안에서 다른 단계를 골라도 됩니다(→ 모바일). 사이즈를 바꿀 때 DS 클래스엔 md:가 안 붙으므로 앱 main.css의 @media (max-width:767px)에서 덮으세요(유틸리티 컨트롤은 모바일 퍼스트 h-12 md:h-10). ⚠️ 입력칸 탭 시 iOS 자동 확대는 아직 규칙 없음(현재 14px, 확대됨) — 새로 정하지 말고 그 섹션을 읽으세요.
  • 상세(읽기전용) 모달: 본문·사용자 입력은 x-text만, 리치 텍스트 아니면 x-html 금지(XSS). ESC는 팝업 열린 동안만 등록/해제.
  • 예외(표준 적용 금지): 컨텍스트 메뉴·드롭다운·피커·자동완성(바깥클릭 닫힘), 이미지 라이트박스(90% dim). (급여/hr-system 예외는 2026-07-29 해제됨.)

패널 폼 공유 규칙. 각 패널만의 규칙은 그 컴포넌트 문서에 있습니다.

셸

  • 카드는 .ds-modal ds-modal--panel — 모달 3종 중 패널 모달입니다. 새 종류를 만들지 마세요.
  • 좌측 메뉴는 폭 220px 고정 <aside> 이고, 항목은 <button> + ds-r-8 입니다.

⚠️ 단계 게이트를 만들지 마세요 카테고리는 자유 이동입니다. 「다음 →」으로 넘기는 단계형이 아니에요. 미완성을 알려야 하면 빨간 점 방식을 쓰고, 이동 자체를 막지 마세요.

⚠️ 입력한 걸 조용히 날리지 마세요 X · 취소 · ESC 는 전부 attemptClosePanel() 한 곳을 지나갑니다(닫기 가드). 어느 하나를 바로 showX = false 로 연결하지 마세요.

⚠️ ESC 는 위에서부터 하나씩만ESC 층의 순서를 바꾸지 마세요. 맨 앞에서 권한 팝업에 양보하고, 레이어를 하나 닫고, 막 닫은 뒤 500ms 는 패널을 닫지 않습니다.

⚠️ 주소 검색 코드를 또 복사하지 마세요 지금 세 컴포넌트에 같은 로더가 각각 복사돼 있습니다(아래). 네 번째를 만들지 말고 공용 유틸로 빼는 것을 먼저 논의하세요.

  • 다크 모드(2026-10-13) — 모달 카드 면은 CSS 가 --color-bg-elevated(라이트 #fff · 다크 neutral-700)로 칠한다. 마크업의 bg-white 는 무해하지만 다크에서는 카드 위 흰 면으로 남으므로 새 모달에 쓰지 않고, 건드리는 파일에서는 지운다. 레거시 .ds-mini-modal 도 같은 토큰. 딤 · 흰 딤(--white)은 유리 토큰(--ds-glass-70)이라 다크에서 어두운 딤이 된다. 확인: Playground 「미리보기 테마 → 다크」.