Skip to content

AI 어시스턴트 (AiAssistant) ​

어시스턴트 도크가 화면을 어떻게 나눠 쓰는지 확인하고, 폭·층·ESC 규칙을 찾아보세요. 이 페이지는 PC(도크 · 플로팅) 입니다 — 직원 앱의 페이지 모드(mode: 'page', 첫 화면 · 캡슐형 입력줄)는 모바일 대화 화면 에 있습니다.

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

Interactive playground ​

미리보기가 둘입니다. UI 적용 뷰는 진입점에서 도크가 열리는 실제 화면이고, 폭 계약 도해는 그 폭이 어떻게 계산되는지입니다.

UI 적용 뷰 — 진입점에서 도크까지 ​

옵션을 바꾸면 미리보기와 코드가 함께 바뀌어요.
미리보기 테마
본문
AI 어시스턴트Beta
AI
어떤 도움이 필요하세요?
오른쪽 아래 그라디언트 버튼이 진입점입니다 — 한 번 누르면 바로 도크로 열려요(표시 방식을 고르는 토글은 없습니다). 열리면 본문이 도크 폭만큼 밀립니다 — 덮는 게 아니라 자리를 나눠 쓰는 거라, 채팅 패널과 겹칠 일이 없어요. 도크로 떠 있는 동안 AI FAB 은 숨고, 그래서 출구는 헤더의 X 하나뿐입니다 — 그건 「닫기」가 아니라 플로팅 버튼으로 되돌리기(undock())라 대화가 남습니다. 껍데기·말풍선·입력줄은 채팅 위젯과 같은 부품이고, 도크에서 달라지는 건 폭 하나(--ds-bubble-max: 85%)예요.
헤더 왼쪽 패널 아이콘이 이전 대화 사이드바(.ds-ai-history, 폭 80~150 · 한 줄 제목 · 날짜 없음)를 켜고 끕니다 — 늘 펼친 채 시작해요. 처음 열면 새 대화 첫 화면(기능 바로가기 — 처음 3개, 넘치면 「기능 전체 보기」)이고, 대화 아래 꼬리표는 아이콘 버튼 xs(복사 · 문제 신고)만 남습니다. 답변 대기 줄은 말풍선 없이 글자만, 입력 중 기능과 맞는 말을 치면 「바로 갈 수 있는 화면」 팝오버가 떠요. 아래 「화면」에서 골라 보세요.
<!-- 같은 요소가 클래스 하나로 갈린다 — 마크업을 두 벌 만들지 않는다 -->
<div data-testid="ai-panel"
     :class="dockActive ? 'ds-ai-dock' : ''"
     :data-dock="dockActive ? 'on' : 'off'"
     class="ds-fab-layer ds-fab--ai ds-chat-panel fixed bottom-26">
  <!-- 헤더: 사이드바 토글(ic-panel) · 타이틀 · Beta · 새 대화 · 되돌리기 -->
  <button class="ds-btn ds-btn--icon ds-btn--muted" @click="toggleHistorySidebar()">…ic-panel</button>

  <div class="flex-1 min-h-0 flex">
    <nav x-show="historySidebarOpen" class="ds-ai-history">  <!-- 열림 · 폭 80~150 -->
      <div role="button" class="ds-ai-history__item is-active">이번 달 팀별 연차 사용률</div>
    </nav>
    <div class="flex-1 min-w-0 flex flex-col">
      <div x-ref="aiMessages" style="--ds-bubble-max: 85%">
        <!-- 새 대화: 기능 바로가기 첫 화면 / 대화: 말풍선 + 꼬리표 아이콘 버튼 xs -->
        <button class="ds-btn ds-btn--solid ds-btn--lightor ds-btn--md ds-r-12 ds-chat-start-action">…</button>
        <button class="ds-btn ds-btn--icon ds-btn--muted ds-btn--xs" aria-label="답변 복사">…ic-copy</button>
      </div>
      <div class="ds-composer-bar"><div class="ds-composer">…ds-input--40</div></div>
    </div>
  </div>
</div>

/* 폭의 정본은 변수 하나다 — 지금 --ds-ai-dock-w: 480px */
body.ds-ai-dock-open { padding-right: var(--ds-ai-dock-w); }

껍데기 · 말풍선 · 입력줄은 채팅 위젯과 같은 부품(.ds-chat-panel · .ds-bubble · .ds-composer)이고, 도크에서 달라지는 것은 폭 하나입니다 → 대화 UI. 도크는 실제 .ds-ai-dock 규칙으로 그리며, 데모가 되덮는 것은 position 하나뿐이에요(진짜는 fixed 라 문서 안 상자에 가둘 수 없습니다).

폭 계약 도해 ​

옵션을 바꾸면 미리보기와 코드가 함께 바뀌어요.
미리보기 테마
LNB
본문 512px
도크 780px
화면 1500px본문 확보분 720px 보장
이 화면의 상한
780pxmax(360, min(800, 1500 − 720))
고른 폭
800px사용자의 값 — 화면이 좁아져도 안 덮어쓴다
그리는 폭
780상한에 물림 — 넓히면 800px 로 돌아온다
채팅 FAB 오른쪽
804px24 + 도크 + 레일 0
도크는 화면을 덮지 않고 본문을 밀어냅니다 — 그래서 채팅 패널과 겹치는 자리가 아예 없어져요. 「고른 폭」과 「그리는 폭」이 따로인 것이 이 컴포넌트의 핵심입니다: 폭을 800으로 두고 화면을 1280까지 좁혔다 다시 넓혀 보세요. 그리는 폭만 상한에 물렸다가 고른 값으로 되돌아옵니다. 한 값으로 합치면 되돌아오지 못하고 굳어요.
/* 폭의 정본은 변수 하나다 — 세 곳이 전부 이걸 읽는다.
   AiAssistant.applyDockLayout() 이 <body> 에 세운다. */
body { --ds-ai-dock-w: 780px; }   /* dockRenderWidth */

body.ds-ai-dock-open                            { padding-right: var(--ds-ai-dock-w); }
body.ds-ai-dock-open .ds-modal-backdrop         { right: var(--ds-ai-dock-w); }
body.ds-ai-dock-open .ds-fab-layer.ds-fab--chat {
  right: calc(24px + var(--ds-ai-dock-w) + var(--ds-fab-dodge, 0px));
}                                               /* = 804px */

화면 폭과 사용자가 고른 폭을 바꿔 가며, 도크가 실제로 결정하는 것 — 화면을 어떻게 나눠 쓰는가 — 을 볼 수 있습니다. 값은 전부 AiAssistant.ts 의 계산식 그대로예요.

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

AI 어시스턴트 도크의 강제 규칙. 상세·근거는 아래 본문. FAB 자체의 배치·색·배지 계약은 채팅·AI FAB 문서가 정본입니다.

모드 — 도크 하나뿐이고, 마크업도 한 벌이다

  • 모드는 클래스 하나(.ds-ai-dock)로만 갈린다 — 마크업을 두 벌 만들지 말 것. 플로팅과 도크는 ai-assistant.html의 같은 요소다. x-if로 갈래를 나누면 Alpine 인스턴스가 갈려 전환할 때마다 대화가 통째로 초기화된다. 도크 클래스가 bottom-26·w-96·h-[560px]·rounded-16·shadow-popover 유틸을 되덮는데, 유틸은 @layer utilities 안이고 DS 규칙은 레이어 밖이라 특이도와 무관하게 이긴다.
  • 어시스턴트는 도크로만 열린다 — 떠 있는 창 모드는 없다. 표시 방식을 고르는 토글도 두지 않는다. 이유는 채팅 패널과의 겹침이다: 384px짜리 둘을 나란히 띄우면 80%가 겹쳐(실측 328×560) 뒤쪽을 읽을 수 없는데, 순서를 z로 바꾸는 것은 불가능하다 — 어시스턴트는 모달(50) 위(65)여야 하고 채팅은 모달 아래(44)여야 해서, 채팅을 어시스턴트 위로 올리면 채팅 > 65 > 50이 되어 모달의 확정 버튼을 덮는다(2026-08 회귀). 도크는 본문을 밀어내고 채팅이 그 왼쪽으로 비키므로 겹침 자체가 0이 된다 — z를 하나도 건드리지 않고 풀리는 유일한 길이다.
  • 태블릿 이하(<1280)에서는 도크 자체가 없다. 화면이 좁아지면 떠 있는 패널로, 다시 넓히면 도크로 자동 전환된다 — 고를 여지가 없으므로 토글도 없다.
  • 1280 밑으로 내려가는 순간 어시스턴트는 접힌다. 떠 있는 창으로 바꿔 두면 그 폭에서 채팅과 80% 겹쳐 뒤쪽을 못 읽는다 — 열어 둔 채 두는 것보다 접는 편이 낫다. 경계를 넘는 순간에만 접는다: 좁은 화면에서 사용자가 직접 연 떠 있는 창까지 리사이즈마다 닫으면 못 쓴다.
  • 모드 판별은 data-dock="on|off"로 한다 — 플로팅과 도크가 같은 요소라 ai-panel의 존재만으로는 구분되지 않는다(e2e 포함).
  • 헤더의 X는 「닫기」가 아니라 「도크 해제」다. 대화를 끝내는 게 아니라 패널을 원래의 플로팅 버튼(FAB)으로 되돌릴 뿐이고, 대화는 같은 인스턴스에 남아 FAB을 다시 누르면 이어진다 — 그래서 라벨도 「닫기」가 아니다(undock(), close()가 아님). 도크에서는 FAB이 비노출이라 이것이 유일한 출구이므로 선택이 아니라 필수다.

폭 — 정본은 변수 하나, 값은 두 개

  • 폭의 정본은 --ds-ai-dock-w 하나다. 본문 밀어내기(body.ds-ai-dock-open의 padding-right) · 모달 딤의 오른쪽 여백 · 채팅 FAB 비킴이 모두 이 변수 하나를 읽는다. 세 곳에 각각 바인딩을 걸지 말 것 — 드래그 한 프레임마다 Alpine이 패널 서브트리를 다시 그려, 임베드된 시트가 매 프레임 가상화 그리드를 재계산한다.
  • 본문 밀어내기는 .app-shell이 아니라 <body>에 건다. 도크는 공용 컴포넌트인데 셸 클래스는 앱마다 다르다. position: fixed인 모달·토스트·FAB은 이 패딩을 무시하므로 각자 따로 비킨다.
  • 폭은 360 ~ min(800, 화면폭 − 720). 화면이 좁아지면 상한이 함께 줄어든다(실측: 1920·1700→800 · 1500→780 · 1400→680 · 1280→560). ⚠️ 줄어들 때 「고른 폭」을 덮어쓰면 안 된다 — 표시만 물리고 사용자가 고른 값은 그대로 둔다(dockRenderWidth vs dockWidth). 예전에는 창을 줄일 때마다 고른 값 자체를 물려서 다시 넓혀도 안 돌아왔다(800 → 900px 로 줄였다 되돌리면 360 으로 굳음, 실측). 상한을 800에 못 박으면 활성 하한인 1280 화면에서 본문이 272px만 남아 표가 통째로 못 읽히게 된다 — PC 경계를 lg→xl로 올린 것과 같은 문제다(브레이크포인트). 하한 360은 실측이다: 패널 헤더가 타이틀까지 세우는 데 336px가 필요하다.
  • 폭 조절 핸들은 키보드로도 움직여야 한다 — role="separator" + aria-orientation="vertical" + aria-valuenow/min/max, ←/→ 16px, Home으로 기본 폭. 마우스 전용 핸들은 접근성 결손이다.
  • 드래그 중에는 iframe의 포인터 이벤트를 죽인다(body.ds-ai-dock-resizing iframe). admin은 급여·근태·드라이브·팀관리를 iframe으로 품는데, 커서가 iframe 위로 들어가는 순간 포인터 이벤트가 자식 문서로 가서 부모가 못 받고 드래그가 그 자리에 굳는다. 같은 이유로 user-select도 함께 잠근다(안 하면 드래그가 본문 텍스트 선택으로 샌다).

다른 크롬과의 관계

  • x-show로 숨은 FAB은 DOM에는 남는다 — 그래서 채팅 FAB의 가로 슬롯을 정하는 body:has(.ds-fab--ai)가 계속 매치한다. body.ds-ai-dock-open .ds-fab-layer.ds-fab--chat(0,3,1)이 특이도로 이 규칙(0,2,1)을 이겨 바깥 슬롯(24px)으로 되돌리고, 배지도 기본(오른쪽 위)으로 되돌린다. 소스 순서에 기대지 말 것.
  • 채팅 FAB의 비킴은 도크와 드로어를 더한다 — calc(24px + var(--ds-ai-dock-w) + var(--ds-fab-dodge)). 도크와 시트 레일은 동시에 열릴 수 있다. 둘 중 하나를 통째로 대입하는 형태로 쓰지 말 것.
  • iframe 안의 도크는 부모 문서에 보이지 않는다 — body.ds-ai-dock-open 도 --ds-ai-dock-w 도 iframe 문서 안의 값이라, admin 이 품은 시트의 「시트 어시스턴트」가 도크로 열려도 admin 의 채팅 FAB 은 제자리(도크 입력줄 위)다. 위젯이 applyDockLayout 마다 hereby:tm:ai-dock({ open, width })을 부모에 보내고, admin 은 그 폭을 --ds-fab-dodge 에 더한다 → FAB 배치 계약. 임베드에서 이 위젯이 그려지는 조건(.ds-fab-embed-owner)도 그 문서에 있다.
  • 도크가 열리면 모달 딤의 오른쪽을 도크 폭만큼 물린다(body.ds-ai-dock-open .ds-modal-backdrop). .ds-modal-backdrop은 inset: 0 + justify-content: center라 도크를 몰라 카드를 뷰포트 정중앙에 세우는데, 도크(--ds-z-fab-ai 65)가 모달(50)보다 위라 카드가 도크에 덮여 확인 버튼을 못 누르게 된다. ⚠️ z를 내려서 해결하지 말 것 — 65는 "모달을 띄운 채 어시스턴트에게 묻기"를 위해 올린 값이다.
  • 떠 있는 상태에서는 채팅과 어시스턴트가 한 번에 하나만 열린다(UI 겹침 방어). 여는 쪽이 hereby:floating-panel-open 을 쏘고 반대쪽이 접는다. 도크에서는 쏘지 않는다 — 채팅이 도크 왼쪽으로 비켜서 겹치지 않으므로 둘 다 열려 있어도 된다(실측 겹침 0).
  • ESC 로 닫히는 것은 「떠 있는」 어시스턴트뿐 — 도크는 닫히지 않는다. 떠 있을 때는 본문 위에 얹힌 팝업이라 ESC 가 자연스럽지만, 도크는 본문을 밀어내고 자리를 차지하는 상시 크롬이라 ESC 의 대상이 아니다 — 모달을 띄운 채로도 쓰라고 만든 것을 키 하나로 접으면 그 전제가 깨진다. 판정은 층 등록의 getEl 이 진다: 도크일 때 null 을 돌려 후보에서 빠진다. 디스패처 규칙 자체는 ESC 순서를 따른다.
  • 말풍선·입력줄은 메신저와 «같은 부품»이다(.ds-bubble · .ds-composer) — 두 대화창이 한 화면에 나란히 뜨므로 한쪽만 바꾸지 말 것. 이 패널만 다른 것은 폭 하나이고, 그건 메시지 목록의 --ds-bubble-max: 85% 가 정한다 → 말풍선 · 입력줄

노출·피드백

  • 위치 안내는 대상 옆에 표시합니다. 등록된 대상 이름과 다음 행동을 위치 안내 툴팁(purple 500 말풍선 · 아이콘 버튼 × — 툴팁 문서)으로 보여 주고, 대상 바깥에 간격을 둔 purple 400 강조선을 그립니다. 강조선은 화면·도크 가장자리에서 잘리지 않게 화면 안으로 줄여 그립니다. 화면 상단 토스트나 채팅에 "테두리로 표시했어요"만 남기지 않습니다. 호버 툴팁과 달리 직접 닫는 안내이며, 대상 버튼의 클릭이나 기존 키보드 포커스를 가로채지 않습니다.
  • 위치 안내에는 자동 종료 시간이 없습니다. 대상 클릭·닫기·ESC·다른 안내 선택·화면/사용자 변경 때 설명과 강조를 함께 정리합니다. 스크롤·도크 폭 변경을 따라가며, 대상이 스크롤 영역 밖에 있으면 잠시 숨깁니다. 설명은 현재 뷰포트 안에 두고 위쪽 공간이 부족하면 아래로 옮깁니다. 공통 구현은 packages/components/src/guidance/와 design-system.css의 .ds-guidance-*가 담당합니다.
  • 메뉴 안의 위치도 실제 컨트롤을 표시합니다. 접힌 메뉴를 먼저 열고, 상태 표시는 실행 위치로 대신 쓰지 않습니다. 강조와 설명은 메뉴 위에 그리되 body.ds-backdrop-open인 동안에는 숨겨 모달 작업을 가리지 않습니다. 메뉴를 여는 것만 허용하며 업무 버튼은 사용자가 직접 누릅니다.
  • 플래그가 꺼져 있으면 렌더 자체를 하지 않는다 — 조직 설정 ai.enabled(서버 ai.status)와 사용자 권한 ai:execute 를 둘 다 만족할 때만 enabled 가 서고, 마크업은 x-if="enabled" 로 통째로 걸려 있다.
  • 노출 판정은 x-effect 로 인증 상태를 구독한다. 스토어 init()(세션 복원)이 컴포넌트 init 보다 늦게 끝나므로 init 에서 한 번 검사하는 방식은 항상 미인증 상태를 본다. $watch 는 $store 경로에 발화하지 않으므로 x-effect="if ($store.hereby.authenticated) activate()" 가 정답이다.
  • 처음 열면 새 대화다(떠 있는 패널·도크·페이지 모드 모두, 2026-10-08) — 페이지를 띄운 뒤 처음 열 때 마지막 대화를 이어 열지 않고 빈 대화(첫 화면)로 시작한다. 대화 목록은 읽어 두어 사이드바(직원 앱은 오른쪽 서랍)에 바로 보인다. 닫았다 다시 열면 닫기 직전 화면이 그대로다.
  • 첫 화면 기능 버튼 = 시스템 어드민 「문서 안내」 기능이다 — 시스템 어드민 › 조직 › AI 어시스턴트의 「문서 안내」(settings.ai.modules.guidance.enabled)를 켜고 서버 연결까지 갖춰져야(ai.status 의 guidance) 「어떤 도움이 필요하세요?」 아래 기능 버튼이 뜬다. 끄면 버튼 없이 기본 빈 화면 안내만 보인다. 버튼 목록은 서버 guidanceCatalog(앱 · 권한으로 거름) 그대로다.
  • 입력 중 키워드가 기능과 맞으면 팝오버로 잇는다(admin, 2026-10-08) — 직원 앱 「바로 갈 수 있는 화면」 팝오버와 같은 마크업(흰 면 · shadow-popover · 줄마다 아이콘 + 이름 + ic-chevron-right, 버튼 line · muted · ml · ds-r-12)이 입력줄 위에 뜬다. 맞추는 대상은 기능 바로가기 카탈로그이고(이름 · 설명 · GUIDANCE_ENTRY_META 의 검색어, 두 글자부터 · 최대 4개 — matchMenuEntries), 누르면 입력을 비우고 그 화면으로 가서 대상을 하이라이트한다(pickMenuEntry → executeGuidanceAction). 맞는 게 없으면 그대로 보내 AI 에게 묻는다. 카탈로그는 첫 화면이 뜰 때나 입력칸에 포커스할 때 한 번 읽는다. 팝오버는 두 모드 모두 입력줄 위에 떠 있다(absolute).
  • 이전 대화는 왼쪽 사이드바다(떠 있는 패널·도크, 2026-10-08) — 헤더 타이틀 왼쪽의 패널 아이콘 버튼(ds-btn--icon --muted + ic-panel, aria-pressed)이 켜고 끈다. 예전 헤더 오른쪽의 「이전 대화 목록」(시계) 버튼과 목록 화면 전환은 없앴다. 사이드바(.ds-ai-history)는 본문 줄 폭의 30% 를 따르되 최소 80 · 최대 150 이고, 행(.ds-ai-history__item, div[role=button])은 제목 한 줄 말줄임만 보이며 날짜는 그리지 않는다(전체 제목은 title 툴팁). 지금 대화는 .is-active. 열 때와, 사이드바가 열린 채 답변이 끝날 때 목록을 다시 읽는다(첫 질문이 제목이 되므로). 늘 펼친 채로 시작한다 — 접어도 그 상태를 기억하지 않아, 페이지를 다시 띄우면 다시 펼쳐져 있다. 페이지 모드(직원 앱)는 화면이 좁아 본문 옆 사이드바 대신 오른쪽 서랍을 쓴다 → 모바일 대화 화면.
  • admin 의 빈 대화 첫 화면은 기능 바로가기 목록이다(guidanceStartScreen: true, 2026-10-08) — 직원 앱 업무 바로가기와 같은 모양(solid · lightor · md · ds-r-12 + .ds-chat-start-action, 한 줄에 하나)으로 서버 카탈로그의 「현재 앱에서 사용할 기능」을 보여 준다. 처음 3개만 보이고(GUIDANCE_START_LIMIT), 더 있으면 그 아래 「기능 전체 보기」(ds-btn-text --md --dark + ic-chevron-down)로 전부 펼친다. 새 대화를 시작하면 다시 3개로 접힌다. 누르면 대화 중 「기능 바로가기」와 같은 executeGuidanceAction 이다. 첫 화면이 떠 있는 동안 위쪽 「기능 바로가기」 버튼·패널은 숨기고 진행 안내 notice 만 남는다. admin 에는 대화 중 「기능 바로가기」 버튼이 없다(2026-10-08 제거) — 기능은 새 대화 첫 화면에서 고르고, 진행 안내 notice 만 남는다. 버튼·패널은 인사 시트(열 위치 보기가 필요)에만 남고, 그 패널 목록도 같은 칩 · 처음 3개 + 「기능 전체 보기」 모양이다. 맨 위 인사는 모바일과 같이 「심볼 + AI」 + 「어떤 도움이 필요하세요?」이며 admin 은 master 심볼(logo-hereby)을 호스트가 넘긴다. 처음 열면 새 대화로 시작하므로 이 첫 화면이 바로 보인다(아래 「처음 열면 새 대화」). 아이콘은 GUIDANCE_START_ICONS(카탈로그 id → 아이콘)이고 없으면 ic-sparkle. 인사 시트는 열 선택이 필요해 켜지 않는다.
  • 답변 꼬리표의 기능 버튼은 아이콘 버튼 ds-btn--icon ds-btn--muted ds-btn--xs 로 통일한다(2026-10-08) — 복사(ic-copy, 복사 뒤 ic-check) · 문제 신고(ic-megaphone — 불편·기능 신고와 같은 아이콘). 실패 말풍선의 문제 신고와 보낸 메시지 복사도 같다. 라벨 글자는 두지 않고 title 을 싱글톤 툴팁이 보여 주며, aria-label 을 반드시 단다. 변경 제안 카드 안의 「문제 신고」는 카드 액션이라 대상이 아니다.
  • 답변 대기 중 상태 줄은 말풍선이 아니다(2026-10-08, admin · 직원 앱 공통) — 「질문을 이해하고 계획하는 중」 같은 상태와 경과 초, 생각 과정은 배경 없이 글자만 보인다(메타 줄과 같은 px-1, 글자 b5). 답변 글자가 흐르기 시작하면(streamText) 받은 말풍선(.ds-bubble--received)이 된다.
  • 답변 위 소요 시간도 숨김(2026-10-08) — 메타 줄에는 시각만 남는다. 답변을 기다리는 동안 올라가는 경과 초는 그대로다.
  • 실패 말풍선의 오류 코드도 숨김(2026-10-08) — rate_limited · timeout 같은 영문 코드는 사용자에게 뜻이 없다. 값(errorCode)은 「문제 신고」에 그대로 첨부된다.
  • 토큰 사용량과 응답 피드백(👍/👎)은 숨김(2026-10-08 디자인 결정) — 마크업은 <template x-if="false"> 로 남겨 두어 되살릴 때 그 조건만 바꾸면 된다. tokenLabel() · rate() 와 서버 저장은 그대로다. ⚠️ 피드백을 숨기는 동안 system-admin 「AI 모니터링」의 👍/👎 열에는 새 값이 쌓이지 않는다.
  • 메시지마다 「복사」 가 있다(보낸 것·받은 것 모두, CS-OKBEST-260923-0004). 마크다운 원문을 평문으로 복사한다. 드래그 선택도 되지만 시트 화면에서는 워크북의 @copy.window 가 Ctrl+C 를 가로채 셀 값을 쓰던 것을 hr-system clipboard.ts 의 「셀 밖 텍스트 선택이면 네이티브 복사」 가드로 풀었다 — 패널에서 다시 user-select 를 만지지 말 것.
  • 다크 모드(2026-10-13) — 이전 대화 목록 .ds-ai-history 면은 bg-alternative, 항목 hover · 선택은 bg-hover, 서랍 .ds-ai-drawer 면은 bg-elevated(다크 700), 서랍 항목 눌림은 bg-sunken, 도크 리사이저 활성은 primary. 라이트는 값이 같다(neutral-50/100/75 · white 와 동일). 확인: Playground 「미리보기 테마 → 다크」.

화면 오른쪽에 붙어 본문을 밀어내는 AI 대화 패널입니다. 팝업이 아니라 상시 크롬이에요 — 모달을 띄운 채로도 "이 화면 뭐예요"를 물을 수 있어야 한다는 것이 이 컴포넌트의 전제이고, 층(65)·ESC·딤 여백 규칙이 전부 거기서 나옵니다.

왜 도크인가 ​

처음에는 채팅 위젯과 같은 떠 있는 패널이었습니다. 그런데 둘 다 384px 라서 나란히 띄우면 80%가 겹쳤어요(실측 328×560).

겹침을 z로 푸는 길은 막혀 있습니다.

필요한 층이유
어시스턴트모달(50) 위 = 65모달을 띄운 채 물어볼 수 있어야 한다
채팅모달(50) 아래 = 44팝업이 뜨면 FAB·패널은 뒤로 가야 한다

채팅을 어시스턴트 위로 올리면 채팅 > 65 > 50 이 되어 모달의 확정 버튼을 덮습니다(2026-08 회귀). 빈 z 칸 어디에 놓아도 같아요. 도크는 본문을 밀어내고 채팅이 그 왼쪽으로 비켜서 겹침 자체를 0으로 만듭니다 — z를 하나도 건드리지 않고 풀리는 유일한 길이었습니다.

여는 법과 닫는 법 ​

  • 열기 — AI FAB 한 번이면 바로 도크입니다. 표시 방식을 고르는 토글은 없어요.
  • 닫기 — 도크 헤더의 X 는 undock() 입니다. 대화를 끝내는 게 아니라 플로팅 버튼으로 되돌리는 것이라, 다시 누르면 하던 대화가 이어집니다. 도크 상태에서는 FAB 이 숨으므로 이 X 가 유일한 출구예요.
  • ESC — 도크는 닫히지 않습니다. 떠 있는 패널일 때만 ESC 대상이에요.

알림은 숫자가 아니라 점 하나 ​

어시스턴트에는 「미확인 N건」이라는 개념이 없습니다. 있는 것은 닫아 둔 사이 답변이 도착했다는 사실뿐이고, 그건 세는 게 아니라 있고 없고예요. 그래서 채팅의 숫자 배지가 아니라 점(.ds-fab__dot) 하나를 씁니다. 켜지는 시점은 응답이 끝났는데 패널이 닫혀 있을 때, 꺼지는 시점은 패널을 열 때입니다 — 닫을 때 끄면 닫는 순간 도착한 답변을 놓쳐요. 모양·색 규칙은 FAB 계약에 있습니다.

어디에 있나 ​

PC 도크 · 플로팅은 지금 admin 한 곳(dashboard.html)에만 있고, 그것도 조직 AI 플래그가 켜져 있을 때만 그려집니다. (직원 앱은 FAB 이 아니라 페이지 모드로 씁니다 — 하단 내비 AI 버튼 → 모바일 대화 화면.) 그래서 다른 앱에서는 채팅 FAB 이 바깥 슬롯(24px)에 혼자 서요 — 채팅을 80px 에 못 박으면 안 되는 이유입니다.

도크와 플로팅 — 같은 마크업, 클래스 하나 차이 ​

도크 (≥1280)플로팅 (<1280)
클래스.ds-ai-dock 추가유틸 그대로(w-96 h-[560px])
판별data-dock="on"data-dock="off"
자리화면 오른쪽 전체 높이우하단, 본문 위에 얹힘
본문padding-right 로 밀림그대로(덮음)
폭360 ~ min(800, 화면폭−720), 드래그 조절384 고정
ESC안 닫힘닫힘
채팅과겹침 0한 번에 하나만 열림
나타남 효과없음translate-y + scale

같은 요소입니다. x-if 로 갈래를 나누면 Alpine 인스턴스가 갈려 전환할 때마다 대화가 초기화돼요.

폭의 세 값 ​

값무엇어디서
dockWidth사용자가 고른 폭드래그·키보드, localStorage(hereby:aiDockWidth)
dockMaxWidth이 화면의 상한max(360, min(800, 화면폭 − 720))
dockRenderWidth실제로 그리는 폭고른 폭을 상한에 물린 값 → --ds-ai-dock-w

⚠️ 둘을 합치면 안 됩니다. 창을 줄일 때 고른 값까지 덮어쓰면 다시 넓혀도 돌아오지 않아요(800으로 쓰다 900px 로 줄였다 되돌리면 360 으로 굳음 — 실측). 위 Playground 에서 고른 폭을 800 으로 두고 화면을 1280 ↔ 1920 으로 오가면 그대로 보입니다.

상태 표시 ​

상태화면비고
생각 중요약된 생각 과정 스트리밍(thinkingText)저장하지 않음
조회 중실행 중 tool 이름(activeTool)끝나면 toolLog 로
답변 중델타 스트리밍(streamText)매 델타마다 하단 고정
실패「응답 생성에 실패했습니다」 말풍선failed: true — 피드백 대상 아님
새 답변FAB 의 점 하나패널을 열면 꺼짐

붙이는 법 ​

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

html
<!-- apps/admin/src/views/dashboard.html -->
<div x-data="aiAssistant({ app: 'admin', onGuidanceAction: (action, signal) => navigateGuidance(action, signal),
  guidanceStartScreen: true, startBrandIcon: 'logo-hereby', startHeading: '어떤 도움이 필요하세요?',
  getContext: () => ({ tab: $store.router.path }) })">
  <!-- include "../../packages/components/html/ai-assistant.html" -->
</div>
옵션타입무엇
appstring위젯이 탑재된 앱 식별자 — 대화 생성 시 함께 저장
getContext() => { tab?, path? }턴마다 호출되는 화면 컨텍스트. 어느 탭에서 물었는지가 답변 품질의 핵심 입력
onGuidanceAction(action, signal) => Promise<void>위치 안내 — 화면을 옮기고 대상에 위치 안내 툴팁을 띄우는 호스트 함수. 없으면 기능 바로가기 · 키워드 팝오버가 꺼진다
guidanceStartScreenboolean빈 대화 첫 화면을 기능 바로가기 목록(처음 3개 + 「기능 전체 보기」)으로 그리고, 대화 중 「기능 바로가기」 버튼을 없애고, 입력 중 키워드 팝오버를 켠다(admin). 인사 시트는 끈다
startHeading · startBrandIconstring첫 화면 맨 위 「심볼 + AI」 · 질문. 심볼은 호스트가 정한다(공용 템플릿에 브랜드 하드코딩 금지)

전제 ​

  • 인증 이후에만 켜진다 — 마크업 루트의 x-effect 가 $store.hereby.authenticated 를 구독한다. 직접 activate() 를 부르지 말 것.
  • 권한 ai:execute + 조직 플래그 ai.enabled 를 둘 다 만족해야 enabled 가 선다. 하나라도 없으면 마크업이 통째로 안 그려진다.
  • CSS 는 정본 한 곳(packages/static/styles/design-system.css)에 있다 — .ds-ai-dock*, body.ds-ai-dock-open, body.ds-ai-dock-resizing. 앱 main.css 에 복사본이 없다.

새 앱에 붙일 때 확인할 것 ​

  1. 그 앱에 채팅 FAB 이 있나 — 있으면 가로 슬롯이 자동으로 갈린다(AI 바깥 24px, 채팅 안쪽 80px). 마크업에서 위치를 정하지 말 것.
  2. 그 앱이 iframe 을 품나 — 품으면 드래그 중 포인터 잠금 규칙이 필요하다(정본 CSS 에 이미 있음).
  3. 그 앱에 오른쪽 드로어·레일이 있나 — 있으면 --ds-fab-dodge 로 비킨다. 도크 폭과 더해진다.

상태·메서드 ​

이름종류무엇
enabled상태권한 + 조직 플래그 통과 여부. 마크업 x-if 의 조건
isOpen상태패널이 열려 있나
dockAllowed상태화면폭 ≥ 1280
dockActivegetterisOpen && dockAllowed — 지금 도크인가
dockWidth / dockRenderWidth상태 / getter고른 폭 / 그리는 폭 → 폭의 세 값
dockMinWidth · dockDefaultWidth · dockMaxWidthgetter360 · 384 · max(360, min(800, W−720))
toggle()메서드FAB 이 부른다. 열 때 새 답변 점을 끈다
undock()메서드헤더 X — 닫기가 아니라 플로팅으로 되돌리기
setDockWidth(px) / nudgeDockWidth(±px)메서드드래그 / 키보드
applyDockLayout()메서드<body> 표식과 폭 변수를 한 번에 반영
send() · onEnter(e)메서드전송 진입점 하나(IME 조합 중 Enter 무시)
historySidebarOpen · toggleHistorySidebar()상태 · 메서드왼쪽 이전 대화 사이드바(떠 있는 패널·도크). 늘 펼친 채 시작하고 접은 상태는 기억하지 않는다
openConversation(id) · newConversation()메서드대화 열기 · 새 대화(처음 열 때도 새 대화로 시작)
historyDrawerOpen · openHistory() · closeHistoryDrawer()상태 · 메서드이전 대화 오른쪽 서랍 — 페이지 모드(직원 앱) 전용. openHistory 는 열고 닫는 토글(호스트의 app-ai-history). 떠 있는 패널·도크는 사이드바를 쓴다
drawerTouchStart/Move/End · historyDrawerDrag메서드 · 상태서랍 손가락 제스처 — 대화에서 왼쪽으로 밀어 열기 · 서랍/딤에서 오른쪽으로 밀어 닫기. 판정은 utils/drawer-swipe.ts
showGuidanceStart · guidanceStartItems · guidanceStartHasMoregetter기능 바로가기 첫 화면 표시 여부 · 보일 칸(처음 3개 또는 전체) · 「기능 전체 보기」 노출
menuMatches · pickMenuEntry(entry)getter · 메서드입력 중 팝오버 결과 · 줄을 눌렀을 때(기능이면 하이라이트, 메뉴면 호스트가 화면 열기)
rate(msg, 'up'|'down')메서드피드백 — 지금 마크업에서 감춰져 있다
destroy()메서드리스너 해제 + 본문 밀어내기 원복(안 하면 오른쪽이 영영 빈다)

CSS 계약 ​

이름값누가 읽나
--ds-ai-dock-w그리는 폭(px)본문 padding · 모달 딤 right · 채팅 FAB right
body.ds-ai-dock-open도크 열림 표식위 세 규칙의 스코프
body.ds-ai-dock-resizing드래그 중iframe { pointer-events: none } + user-select 잠금
--ds-z-fab-ai65.ds-fab-layer.ds-fab--ai — 모달(50)·모달위모달(60) 위, 컨펌(70)·권한(75) 아래
--ds-ai-dock-w 기본0px변수를 안 세운 앱·화면은 아무 변화가 없다

data-testid ​

testid무엇
ai-fab플로팅 버튼(도크에서는 display:none, DOM 에는 남음)
ai-panel패널. data-dock="on|off" 로 모드 판별
ai-dock-resizer폭 조절 핸들(role="separator")
ai-history-toggle · ai-history-sidebar헤더 왼쪽 사이드바 켜기/끄기(aria-pressed) · 이전 대화 사이드바(행은 ai-conversation-row, data-id)
ai-history-drawer · ai-history-dim · ai-new-chat-page페이지 모드 이전 대화 서랍 · 그 뒤 딤(탭하면 닫힘) · 서랍 맨 위 「새 대화」(행은 같은 ai-conversation-row)
ai-guidance-start · ai-guidance-start-action · ai-guidance-start-all기능 바로가기 첫 화면 · 기능 칸(data-id) · 「기능 전체 보기」
ai-menu-matches · ai-menu-match입력 중 「바로 갈 수 있는 화면」 팝오버 · 줄(data-id)
ai-copy · ai-copy-user · ai-report · ai-report-failure꼬리표 아이콘 버튼(답변 복사 · 보낸 메시지 복사 · 문제 신고 · 실패 신고)

단일 출처 ​

  • 로직 — packages/components/src/components/AiAssistant.ts (DOCK_* 상수가 모든 숫자의 정본)
  • 마크업 — packages/components/html/ai-assistant.html(FAB · 도크 껍데기) · ai-assistant-conversation.html(헤더 · 사이드바 · 말풍선 · 입력줄 — 직원 앱 페이지 모드와 한 벌)
  • CSS — packages/static/styles/design-system.css (.ds-ai-dock, .ds-ai-dock__resizer, body.ds-ai-dock-*, .ds-ai-history*, 위치 안내 .ds-guidance-*) — 앱 복사본 없음
  • 아이콘 — ic-sparkles(닫힘) · ic-chevron-down(열림) · ic-panel(사이드바) · ic-pencil-square(새 대화) · ic-copy · ic-megaphone(문제 신고)

변경 이력 ​

  • 2026-10-08 — 도크 Playground(DemoChatSurface 도크 모드)를 실제 구현과 같게 다시 그림: 사이드바 · 헤더(패널 아이콘 · 새 대화) · 첫 화면 · 꼬리표 아이콘 버튼 · 대기 상태 줄 · 키워드 팝오버, 「화면」 선택 추가
  • 2026-10-08 — 답변 대기 상태 줄(「질문을 이해하고 계획하는 중」 + 경과 초)은 말풍선 배경 없이 글자만(admin · 직원 앱)
  • 2026-10-08 — 답변 위 「답변 소요 ○초」 · 「처리 시간」 숨김(기다리는 동안의 경과 초는 유지)
  • 2026-10-08 — 실패 말풍선 아래 「오류 코드 …」 숨김(신고 첨부는 유지)
  • 2026-10-08 — 입력 중 키워드가 기능과 맞으면 「바로 갈 수 있는 화면」 팝오버로 하이라이트 연결(admin)
  • 2026-10-08 — admin 대화 중 「기능 바로가기」 버튼 제거(첫 화면에서만 고름)
  • 2026-10-08 — 대화 중 「기능 바로가기」 패널도 모바일 업무 바로가기 모양으로 · 입력칸 32 → 40
  • 2026-10-08 — 처음 열면 마지막 대화 대신 새 대화로 시작(떠 있는 패널·도크·직원 앱 페이지 모드)
  • 2026-10-08 — 이전 대화를 헤더 시계 버튼 → 왼쪽 사이드바(패널 아이콘으로 켜고 끔, 폭 80~150, 한 줄 제목 · 날짜 숨김)로
  • 2026-10-08 — admin 빈 대화 첫 화면을 기능 바로가기 목록(처음 3개 + 「기능 전체 보기」)으로
  • 2026-10-08 — 답변 꼬리표 기능 버튼(복사 · 문제 신고)을 아이콘 버튼 xs 로 통일, 토큰 사용량 · 👍/👎 숨김
  • 2026-10-08 — 위치 안내가 purple 500 툴팁(아이콘 버튼 ×)으로 바뀌고 강조선이 purple 400 · 화면/도크 가장자리에서 잘리지 않게 됐습니다 → 위치 안내 툴팁
  • 2026-09-09 — 이 페이지 신설. 채팅 문서에 얹혀 있던 「AI 어시스턴트 도크」 절을 여기로 옮기고 폭 계약 Playground 를 붙였습니다 → 변경 이력
  • 2026-09-04 — 도크 도입(플로팅 창 모드 폐지) · ESC 공용 디스패처 · 1280 미만 접힘 · 응답 피드백 감춤

남은 것 ​

  • 응답 피드백(👍/👎) · 토큰 사용량 — 2026-10-08 디자인 결정으로 꼬리표에서 감춰 둔 상태입니다. 되살릴 때는 ai-assistant-conversation.html 의 x-if="false" 두 곳을 바꿉니다.
  • admin 밖으로 확장 — 지금은 admin 전용이라 다른 앱의 FAB 슬롯 계약이 실전에서 검증되지 않았습니다.