Skip to content

불편·오류 신고 (CsReport) ​

팝업이 떠 있어도 신고에 닿을 수 있는지, 그 화면이 캡처에 담기는지를 확인하고 규칙을 찾아보세요.

Alpine 컴포넌트 + DS CSS · Web / Mobile · 진행 중(WIP)

Interactive playground ​

옵션을 바꾸면 미리보기와 코드가 함께 바뀌어요.
미리보기 테마
  1. 70확인·알림 다이얼로그--ds-z-modal-alert없음
  2. 68신고 모달--ds-z-modal-report없음
  3. 65딤 위 신고 버튼--ds-z-report-fab떠 있음
  4. 65어시스턴트 도크·FAB--ds-z-fab-ai없음
  5. 60모달 위 모달--ds-z-modal-top떠 있음
  6. 50모달 + 딤--ds-z-modal떠 있음
  7. 44채팅 FAB·패널--ds-z-fab떠 있음
  8. 40페이지 헤더 (헤더 안 신고 버튼)--ds-z-page-header떠 있음
신고 버튼을 누를 수 있어요.팝업이 문제일 때가 신고가 가장 필요한 순간이라, 그때만 뜨는 대체 진입점이에요.
지금 캡처하면 — captureFilter
  • 본문 화면
  • 열려 있는 모달과 그 딤
  • 어시스턴트 도크 · 채팅 패널 · 시트 레일 — 지금은 없음
  • 신고 UI 자신 (data-cs-report-host)
  • 확인·알림 다이얼로그 (.ds-modal-backdrop--top)
  • display:none 서브트리 · 교차 출처 이미지
신고는 화면이 이상할 때 부르는 것이라, 그 화면이 이미 팝업인 경우가 흔합니다 — 그래서 층 순서가 이 컴포넌트의 규격이에요. 「모달 위 모달」에서 버튼을 55 로 두면 딤에 그대로 깔리고, 신고 모달을 기본 50 으로 두면 밑으로 들어가 「눌렀는데 아무 일도 안 난다」가 됩니다. 반대로 확인 다이얼로그(70)보다 위로 올리지도 않습니다 — 결정을 묻는 팝업을 가리면 안 되니까요.
/* 보임/숨김을 CSS 가 정한다 — MutationObserver 를 두지 않는다(admin 2만 노드) */
body:has(.ds-modal-backdrop:not([style*="display: none"])) .ds-report-fab {
  display: flex;
}
.ds-report-fab {
  z-index: var(--ds-z-report-fab);   /* 65 — 겹쳐 뜬 모달(60) 위 */
  bottom: 104px;                     /* FAB 줄보다 한 칸 위 — AI FAB 과 좌표가 겹친다 */
  right: calc(24px + var(--ds-ai-dock-w, 0px) + var(--ds-fab-dodge, 0px));
}
.ds-modal-backdrop--report {
  z-index: var(--ds-z-modal-report); /* 68 — 어떤 팝업 위에서도 열린다 */
}

/* 지금 이 장면: 딤 위 버튼 보임 · ESC → 모달 위 모달 (60) */

배경에 뭐가 떠 있는지를 바꿔 가며 딤 위 신고 버튼(65)과 신고 모달(68)이 어디에 서는지, 그리고 그 화면을 캡처하면 무엇이 담기고 무엇이 빠지는지를 볼 수 있습니다. 모달 자체는 평범한 일반 모달이라 여기서 다시 그리지 않고, 두 번 회귀한 두 축(닿을 수 있나 · 담기나)만 세웠습니다.

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

불편·오류 신고의 강제 규칙. 상세·근거는 아래 본문. FAB 세트의 가로 자리·비킴 계약은 채팅·AI FAB 문서가 정본입니다.

층 — 이 컴포넌트의 규격 그 자체다

  • 신고 모달은 --report 백드롭(z 68)이다 — 기본(50)을 쓰지 말 것. 신고는 화면이 이상할 때 부르는 것이라 그 화면이 이미 모달이거나 모달 위 모달(60)인 경우가 흔하다. 기본이면 그 아래로 들어가 「눌렀는데 아무 일도 안 난다」가 된다(2026-09-04 사용자 리포트).
  • 딤 위 신고 버튼 .ds-report-fab 은 z 65 — 어시스턴트와 같은 층이다. 55 로 두면 겹쳐 뜬 모달(60)(admin 「근무 설정」 패널 위 「휴가/연차 유형」 같은 흔한 조합)의 딤에 그대로 깔린다.
  • 컨펌 다이얼로그(70)·권한 팝업(75)보다 위로 올리지 말 것. 결정을 묻는 팝업을 가리면 안 되고, 신고 모달 안에서 띄우는 확인 팝업도 이 순서라야 보인다. 오류 다이얼로그에는 「리포트 보내기」가 따로 있어 그 장면의 진입점은 그쪽이다.
  • ⚠️ 헤더 안 버튼만 따로 올리는 방법은 없다. 헤더 래퍼의 z-index(그리고 position: sticky)가 쌓임 맥락을 만들어, 그 안의 요소는 아무리 높은 z 를 줘도 바깥과 겨루지 못한다 — 자식에 position: fixed 를 줘도 같다. 한 번 이 길로 갔다가 되돌렸다. 딤 위 버튼이 별도 요소인 이유가 이것이다.

딤 위 버튼 — 상시 FAB 이 아니다

  • 뜨는 조건은 「팝업이 떠 있을 때」뿐이고, 판정은 CSS 가 한다 — body:has(.ds-modal-backdrop:not([style*="display: none"])) . MutationObserver 를 쓰지 말 것: admin 은 2만 노드라 x-show 토글마다 도는 관찰 비용이 버튼 하나 값어치를 넘는다.
  • ⚠️ FAB 줄이 아니라 한 칸 위(bottom: 104px)에 선다. 가로 계약이 AI FAB 과 같아 같은 줄에 두면 좌표가 정확히 겹친다(실측 둘 다 x=1532). 둘 다 65 라 겹치면 그리는 순서가 승부를 가르는데, 그건 include 순서에 따라 화면마다 달라진다 — 눈에는 안 보이고 클릭만 엉뚱한 데로 가는 고장이다.
  • 면은 흰색 + 어두운 글리프다. 채팅(단색 다크)·AI(그라디언트)와 셋이 세로로 늘어서므로 면으로 구분된다. 테두리(--color-line-normal)는 장식이 아니다 — 흰 원이 딤 위에 뜨면 경계가 뭉개진다.
  • ⚠️ <button> 이 아니라 <div role="button"> 이다. 전역 blanket 규칙이 모든 <button> 의 border-radius 를 4px 로 강제해서, 버튼으로 두면 원이 아니라 모난 사각으로 렌더된다(실측). 채팅·AI FAB 도 같은 이유로 div 다 → radius. 대신 키보드 조작(role/tabindex/Enter·Space)은 직접 달아야 한다.
  • data-ds-tip="top" 을 명시한다. 툴팁 싱글톤은 .ds-btn--icon·.icon-btn 만 자동으로 잡는데 이 버튼은 둘 다 아니라, 빠뜨리면 브라우저 기본 툴팁이 뜬다. 라벨은 title 이 지고 data-ds-tip 은 위치만 고른다 → 툴팁
  • 신고 모달이 열려 있는 동안에는 숨긴다 — 그때 딤은 이 모달의 것이고, 신고 중에 신고 버튼이 또 떠 있으면 무엇을 누르는지가 흐려진다.

캡처 — 늘리면 조용히 비어 간다

  • ⚠️ 화면에 보이는 것은 웬만하면 빼지 말 것. 예전에는 클래스만 보고 .ds-modal-backdrop(모든 팝업)과 .ds-fab-layer(채팅·어시스턴트 패널, 시트 레일)를 통째로 걸렀는데, 그러면 사용자가 신고하려는 그 화면이 그림에서 사라진다 — 팝업이나 어시스턴트가 문제일 때가 신고가 가장 필요한 순간인데 정작 증거가 빠진다(2026-09-04 사용자 리포트 2건, 하루에 두 번).
  • 지금 빼는 것은 셋뿐이다 — 신고 UI 자신(data-cs-report-host — 모달과 딤 위 버튼이 둘 다 이 안에 있다), 확인·알림 다이얼로그(.ds-modal-backdrop--top), 그리고 iframe 의 안쪽 문서(아래). 여기에 항목을 더하려면 cs-report-capture.test.ts 에 회귀를 함께 넣을 것.
  • 임베드(iframe)는 자리만 남기고 회색으로 칠한다 — 안쪽을 끌어오지 말 것. html-to-image 는 같은 출처 iframe 이면 안쪽 <body> 를 두 번 복제하고 그 위에 iframe 의 박스 스타일을 덮어씌운다(admin 은 dev·prod 모두 같은 출처 임베드). 임베드된 시트(수만 노드)까지 끌어오면 10초 안에 못 끝나고, 임베드 앱의 position: fixed 딤이 부모 화면 전체를 덮는다. captureFilter 가 다른 문서의 노드를 거르고, replaceEmbedFramesInClone 이 클론 안에서 iframe, body body 를 회색 상자 + 안내 한 줄로 바꾼다 — 캔버스에 나중에 덧칠하지 않는 이유는 임베드 위에 뜬 모달이 회색 상자 위에 그려져야 하기 때문이다. 회색으로 남긴 경로는 컨텍스트 embedFrames 로 전송되고, 미리보기 아래 노티스가 사용자에게 알린다(요구사항 §5.1).
  • 스크롤 위치는 스냅 마커로 되살린다 — 자식을 transform 으로 밀지 말 것. html-to-image 는 스크롤을 옮기지 않아 시트를 오른쪽·아래로 넘겨 보던 화면이 맨 왼쪽·맨 위로 찍혔다(2026-09-14 재현, 온보딩 9/9 「누락」). markScrolledElements 가 라이브 DOM 에 data-cs-scroll="left,top" 을 붙이고, restoreScrollInClone 이 클론 컨테이너에 scroll-snap-type: both mandatory + 그 오프셋 자리의 1px scroll-snap-align: start 마커를 심는다 — SVG 이미지 안에서도 브라우저가 첫 레이아웃에서 그 자리로 스냅한다(hidden·auto·scroll 실측). 진짜 스크롤이라 sticky 헤더·고정 열이 화면과 같은 자리에 선다. 자식들을 transform 으로 밀어 올리면 sticky 가 같이 밀려 헤더가 사라진다.
  • 화이트리스트에는 Tailwind 4 의 개별 프로퍼티(translate·rotate·scale)가 들어 있어야 한다. translate-*/rotate-*/scale-* 유틸리티는 transform 이 아니라 이 프로퍼티를 쓴다 — 빠지면 -translate-x-1/2 로 가운데 맞춘 팝오버·드롭다운·토글 손잡이가 엉뚱한 자리에 그려져 옆 요소를 가린다. line-clamp-N 의 -webkit-line-clamp·-webkit-box-orient 도 같다(display 만 복사되면 줄 제한이 풀려 아래 행을 밀어낸다).
  • 도크 폭만큼 크롭하지 말 것 — 도크가 열려 있으면 밀려난 본문과 도크가 함께 찍히는 것이 맞다.
  • 성능 가드 두 개는 지운 게 아니라 필수다 — ① display:none 서브트리 통째 건너뛰기(admin 은 숨김 마크업이 2만+ 노드라 안 자르면 캡처가 수십 초, 실측 27,768 노드), ② includeStyleProperties 화이트리스트(기본값은 모든 computed style 복사라 Tailwind 4 커스텀 프로퍼티 수백 개 × 요소 수로 메인 스레드가 30초 이상 멈췄다).
  • toCanvas 를 쓰지 말 것 — toSvg 까지만 쓰고 나머지는 직접 한다. 중간 SVG 의 Alpine 속성(:class·@click)은 XML 로 유효하지 않은 이름이라 이미지 로드가 조용히(onerror 이벤트만) 실패한다. sanitizeCaptureXml() 이 그 속성과 -- 를 품은 주석을 지우는데, toCanvas 에는 끼어들 훅이 없다.
  • 교차 출처 이미지는 캡처에서 뺀다(자리는 남는다). S3 presigned 아바타처럼 화면에는 잘 보이는 이미지가 CORS 없이 인라인에 실패하면 캡처 전체가 reject 된다(2026-09-03 실측, 로컬에서는 재현되지 않음).
  • 캡처 상한은 긴 변 1920px · 제한 시간 10초. 3초로 두면 admin(dev)의 SVG 래스터화만 ~6.3초가 걸려 정상 캡처가 전부 실패 폴백으로 빠진다.

진입점과 개인정보

  • 진입점은 넷이고 도착점은 하나다 — 헤더 버튼 · 딤 위 버튼 · Shift+F1 · 오류 다이얼로그의 「리포트 보내기」. 전부 hereby:cs-report-open 이벤트를 쏜다. 다섯 번째 입구를 만들지 말 것(어시스턴트 안에 있던 진입점은 걷어냈다).
  • 헤더 버튼은 호스트가 있을 때만 보인다(PageHeader.reportAvailable → [data-cs-report-host] 조회). include 를 빠뜨린 앱에서 눌러도 아무 일 없는 죽은 버튼을 막는다.
  • 오류 다이얼로그는 뜨기 «직전» 에 예비 캡처를 쏜다(hereby:cs-report-precapture, 유효 60초). 사용자가 신고하려는 것은 다이얼로그가 아니라 그 뒤 화면이라, 다이얼로그가 덮기 전 그림이 필요하다.
  • [data-cs-mask] 는 캡처 직전 검은 블록으로 자동 마스킹된다. 민감한 값을 새로 그리면 이 속성을 붙일 것 — 캡처는 사람이 열어 보기 전에는 무엇이 담겼는지 아무도 모른다.
  • 사용자 드래그 가림은 이미지에 굽는다(burn-in) — 원본은 전송되지 않는다. 되돌릴 수 없으므로 안내 문구(「가린 부분은 복구되지 않아요」)를 지우지 말 것.
  • 접수 완료는 토스트가 아니라 모달 안에 남긴다 — 접수번호를 전화로 부를 수 있어야 한다. 복사 버튼이 실패해도 번호는 화면에 계속 보인다.

사용자가 스크린샷·최근 로그·환경 정보를 자동으로 붙여 불편이나 오류를 접수하는 모달입니다. 신고는 화면이 이상할 때 부르는 것이라, 이 컴포넌트의 어려움은 폼이 아니라 그 순간에 닿을 수 있는가와 그 화면이 증거로 남는가 둘입니다.

네 개의 입구, 하나의 도착점 ​

입구언제어디에
페이지 헤더 「불편·기능 신고」평상시PageHeader — 호스트가 있는 화면에만
딤 위 버튼 .ds-report-fab팝업이 떠 있을 때화면 우하단, FAB 줄보다 한 칸 위
Shift+F1언제나전역 단축키(입력 중에도 안전)
오류 다이얼로그의 「리포트 보내기」오류가 났을 때ds-dialog

넷 다 hereby:cs-report-open 이벤트를 쏘고, 같은 모달이 열립니다. 앱 코드에서도 같은 이벤트로 열 수 있어요.

왜 딤 위에 버튼이 하나 더 있나 ​

팝업이 뜨면 헤더의 신고 버튼이 딤에 가려 못 누릅니다. 그런데 정작 팝업이 문제일 때가 신고가 가장 필요한 순간이에요. 그래서 그때만 뜨는 대체 진입점을 둡니다.

헤더 안의 버튼만 위로 올리는 방법은 없습니다 — 헤더 래퍼가 쌓임 맥락을 만들어 그 안의 z 는 바깥과 겨루지 못하고, 자식에 position: fixed 를 줘도 마찬가지예요. 위 Playground 에서 「모달 위 모달」을 골라 보면 65 가 왜 필요한 값인지 보입니다.

무엇이 함께 전송되나 ​

폼 안에 「함께 전송되는 정보」 를 펼쳐 볼 수 있게 두었습니다(투명성). 앱·화면 주소·앱 버전·브라우저·최근 로그 건수·오류 정보가 실제 값으로 보이고, 로그는 스위치로 끌 수 있어요.

  • 자동 마스킹 — [data-cs-mask] 가 붙은 요소는 캡처 직전 검은 블록으로 덮입니다.
  • 수동 가림 — 미리보기 위를 드래그하면 그 자리가 이미지에 구워집니다. 되돌릴 수 없어요.
  • 첨부 — 파일 최대 3개, 각 10MB.

접수 후 ​

접수번호는 토스트가 아니라 모달 안에 남습니다 — 전화로 부를 수 있어야 하니까요. 「처리 결과를 앱 알림으로 받을게요」를 켜 두면 처리 시 알림이 옵니다.

어디에 있나 ​

9개 앱에 들어가 있습니다 — admin · hr-system · employee-app · team-manager · general-affairs · inventory · drive · management-dashboard · module-manager. 인증이 없는 landing 에는 없습니다.

층 지도 ​

z무엇토큰
75권한 팝업--ds-z-modal-system
70확인·알림 다이얼로그--ds-z-modal-alert
68신고 모달--ds-z-modal-report
65딤 위 신고 버튼 · 어시스턴트 도크--ds-z-report-fab · --ds-z-fab-ai
60모달 위 모달--ds-z-modal-top
50모달 + 딤--ds-z-modal
44채팅 FAB·패널--ds-z-fab
40페이지 헤더(헤더 안 신고 버튼)--ds-z-page-header

68과 65 사이가 이 컴포넌트의 자리입니다. 아래로 내리면 팝업에 깔리고, 위로 올리면 결정을 묻는 팝업을 가립니다.

화면 두 장 ​

phase화면푸터
form스크린샷 → 유형 → 제목 → 설명 → 파일 → 자동 첨부 안내 → 회신 동의취소 · 보내기
done접수번호 + 복사닫기

스크린샷의 네 상태 ​

screenshotState화면비고
capturing영역 링 .ds-spinner버튼 안이 아니라 영역 로딩 → 스피너
ready미리보기 캔버스 + 드래그 가림「다시 찍기」·「제외」
failed · none.ds-notice--info 안내「파일로 추가해 주세요」

전송 중 버튼 안의 로딩은 아이콘 스피너(ic-spinner)입니다 — 두 종류를 섞지 않습니다.

이 화면이 쓰는 DS 부품 ​

일반 모달 · 세그먼트(유형 4종) · 노티스(--info 안내 / --danger 전송 실패) · 스피너 두 종류 · 스위치 · 툴팁. 새로 그린 것은 딤 위 버튼 하나뿐입니다.

붙이는 법 ​

컴포넌트 등록은 공용 플러그인이 이미 합니다. 앱은 index.html 에 호스트 한 줄 + include 한 줄만 놓으면 돼요.

html
<!-- apps/<app>/index.html — 앱 셸 안, 라우팅 밖 -->
<div x-data="csReport()" data-cs-report-host>
  <!-- include "../../packages/components/html/cs-report.html" -->
</div>

⚠️ data-cs-report-host 는 장식이 아닙니다. 두 가지가 이 속성에 걸려 있어요.

  1. 캡처 제외 — 이 속성이 없으면 신고 모달이 자기 자신을 찍습니다.
  2. 헤더 버튼 노출 — PageHeader 가 이 요소를 찾아보고 없으면 버튼을 감춥니다(죽은 버튼 방지).

전제 ​

  • SDK 플러그인에 appId 를 넘겨야 한다. 스토어의 csAppId 가 비면 전송 단계에서 「앱 설정(appId)이 없어 리포트를 보낼 수 없어요」로 막힌다.
  • 로그·환경 수집기는 SDK 가 이미 돌린다($store.hereby.csReport). 컴포넌트는 필요할 때 함수로 읽기만 한다 — 수집기 배열을 스토어에 직접 담으면 Alpine 이 reactive 로 감싸 push 마다 재렌더된다.
  • html-to-image 는 동적 import 다. 초기 번들에 들어가지 않으므로 신고를 처음 열 때 한 번 받아온다.

여는 법 ​

js
// 앱 어디서든
window.dispatchEvent(new CustomEvent("hereby:cs-report-open"));

오류 다이얼로그를 직접 띄우는 코드라면, 다이얼로그가 화면을 덮기 직전 예비 캡처를 쏘면 됩니다(공용 showError 는 이미 그렇게 합니다).

js
window.dispatchEvent(new CustomEvent("hereby:cs-report-precapture"));

상태·메서드 ​

이름종류무엇
isOpen · phase상태열림 / form | done
type상태bug · inconvenience · feature_request · question
screenshotState상태none · capturing · ready · failed
includeLogs · notifyReporter상태로그 첨부 / 결과 알림 동의 (기본 켬)
errorSnapshot상태수집기의 마지막 오류(5초 TTL) — 없으면 유형이 inconvenience 로 시작
autoInfogetter「함께 전송되는 정보」에 보여 줄 실제 값
open() · close()메서드이벤트·단축키가 부른다. 열 때 폼을 초기화한다
retake() · removeScreenshot()메서드다시 찍기 / 제외
maskStart/Move/End()메서드드래그 가림 — maskEnd 가 마스터 캔버스에 굽는다
onFilesPicked(e) · removeFile(i)메서드최대 3개 · 각 10MB
submit()메서드첨부 업로드(presigned PUT) → csReport.create
copyReportNumber()메서드실패해도 조용히 무시(번호는 화면에 남는다)

순수 함수 (테스트 대상) ​

함수무엇회귀 테스트
captureFilter(node)캡처에서 뺄 노드 판정cs-report-capture.test.ts
shouldSkipCaptureImage(...)인라인 불가 이미지 판정(교차 출처·로드 실패)〃
sanitizeCaptureXml(xml)Alpine 속성·-- 주석 제거〃
markScrolledElements(doc) · restoreScrollInClone(parsed)스크롤 위치 기록 → 클론에 스냅 마커〃
replaceEmbedFramesInClone(parsed, label, colors) · embedFramePath(src, title)iframe 자리를 회색 상자로 · 컨텍스트에 남길 경로〃
isReportShortcut(e)Shift+F1CsReport.test.ts

이것들은 전부 「틀려도 안 터지는」 로직입니다 — 필터가 과하면 그림이 조용히 비고, XML 정리가 빠지면 이미지가 조용히 실패하고, 스크롤 복원이 빠지면 보던 셀이 조용히 없어집니다. 옛 필터를 되살리면 정확히 두 케이스만 깨지는 것까지 테스트가 고정해 두었어요.

상수 ​

이름값왜
CAPTURE_MAX_EDGE1920긴 변 상한
CAPTURE_TIMEOUT_MS10,0003초면 정상 캡처가 전부 실패 폴백으로 빠짐(실측 6.3초)
PRECAPTURE_TTL_MS60,000오류를 읽고 버튼을 누르기까지의 여유
MAX_USER_FILES · MAX_USER_FILE_BYTES3 · 10MB첨부 상한

data-testid ​

cs-report-fab · cs-report-modal · cs-report-close · cs-report-retake · cs-report-preview · cs-report-title · cs-report-desc · cs-report-auto-info · cs-report-include-logs · cs-report-notify · cs-report-submit · cs-report-number

단일 출처 ​

  • 로직 — packages/components/src/components/CsReport.ts
  • 마크업 — packages/components/html/cs-report.html (모달 + 딤 위 버튼이 한 파일)
  • CSS — packages/static/styles/design-system.css (.ds-report-fab, .ds-modal-backdrop--report) — 앱 복사본 없음
  • 서버 — packages/api/src/routers/cs-report.router.ts · 운영자 화면은 tools/system-admin
  • 아이콘 — ic-megaphone

변경 이력 ​

  • 2026-09-14 — 스크롤 위치 복원(스냅 마커) · 임베드 iframe 회색 처리 + embedFrames 컨텍스트 + 안내 노티스 · 화이트리스트에 translate/rotate/scale·line-clamp 추가 (온보딩 9/9 「가림/누락」 CS)
  • 2026-09-09 — 이 페이지 신설. 채팅 문서에 얹혀 있던 신고 FAB·캡처 규칙을 여기로 옮기고 층 Playground 를 붙였습니다 → 변경 이력
  • 2026-09-04 — 딤 위 버튼(65) 도입 · 캡처 필터를 두 대상으로 축소 · ESC 공용 디스패처 등록
  • 2026-09-02 — 캡처 성능 가드(숨김 서브트리 · 스타일 화이트리스트) · 제한 시간 3초 → 10초

남은 것 ​

  • 유형 타입이 마크업보다 좁습니다. 세그먼트는 네 갈래(bug·inconvenience·feature_request·question)를 주는데 컴포넌트의 type 유니온에는 feature_request 가 빠져 있어요. 런타임은 정상이지만(서버 CsReportType 에는 있음) 타입만 보면 「기능 요청」이 없는 것처럼 읽힙니다 — 유니온을 손으로 적지 말고 CsReportType 을 쓰는 편이 맞습니다.
  • 모바일 화면 검증 — employee-app 에도 들어가 있지만 좁은 화면에서의 미리보기·드래그 가림은 아직 정리된 규칙이 없습니다.