Skip to content

현재 사용법 (2026-09-10, 6fbaee7f): 폴더·업로드·접근·내부 공유 · 기능 마스터. 아래는 작성 당시의 변경 설명이며 현재 전체 동작·재측정 결과를 뜻하지 않습니다.

드라이브 폴더 트리·미리보기 창·목록 표 정비 ​

사용자가 보는 변화 ​

드라이브(조직 공유 폴더·파일 보관함, admin LNB 기타 > 드라이브)에서 로그인 직후 목록이 빈 채로 굳던 문제를 고쳤습니다. 좌측 폴더 트리를 admin 「팀 관리」 화면과 같은 모양(행 간격·선택색·계층 연결선·폭 조절)으로 바꾸고, 폴더 검색과 「전체」로 돌아가는 행을 추가했습니다. 파일을 클릭하면 뜨는 미리보기 창은 다운로드·새 탭 열기·파일 크기를 제목 옆 한 줄로 옮기고, 다운로드 버튼을 눌러도 실제로는 새 탭만 열리고 파일이 저장되지 않던 버그를 고쳤습니다. 목록 표는 긴 값이 잘려 보이게(말줄임) 정리했고, 검색어를 입력하면 일치하는 부분이 강조 표시됩니다.

폴더 트리 + 목록검색어 하이라이트
폴더트리 목록검색어 하이라이트
좌측 트리 검색미리보기 창
폴더트리 검색미리보기 창

배경 — 지금까지의 동작 ​

드라이브는 Alpine.js x-data 컴포넌트 하나(apps/drive/src/app.ts의 appComponent())가 화면 전체 상태를 들고, apps/drive/src/views/dashboard.html이 좌측 폴더 트리와 우측 파일 목록 표를 그립니다. 파일을 클릭하면 openPreview()가 preview 상태 객체를 채우고, apps/drive/src/views/modals/preview-modal.html이 그 상태를 읽어 미리보기 창을 그립니다.

아이콘은 화면 코드가 직접 SVG를 그리지 않고 packages/components/src/icons.ts의 이름-도형 사전(레지스트리)에서 $icon('이름')으로 가져다 씁니다. 이 사전은 2026년 8월 PR #544(feat(icons): consolidate icons into one registry callable from anywhere)에서 화면 곳곳에 흩어져 있던 인라인 SVG 1,016곳을 하나로 통합하며 만들어졌습니다.

로그인 직후 목록이 비어 보이던 문제 ​

Alpine은 x-data 객체에 init()이 있으면 그 요소가 화면에 붙는 즉시 자동으로 호출합니다. 사용자가 아직 로그인 화면에 머무는 순간에도 드라이브 컴포넌트의 init()은 이미 돌고 있었습니다. 그 자리에서 파일 목록을 불러오면 인증 토큰이 없어 401로 끝납니다. 로그인은 페이지 이동 없이(history.pushState) 끝나기 때문에 최초 로드가 다시 일어나지 않고, 화면은 오류 표시 없이 「데이터 없음」 상태로 굳었습니다.

변경 전

변경 후

packages/components/src/utils/auth-ready.ts에 whenAuthenticated(ctx)를 추가했습니다. 이미 인증돼 있으면 즉시 반환하고, 아니면 $store.hereby.authenticated를 지켜보다 참이 되는 순간 resolve하는 프로미스를 돌려줍니다. 데이터 로드 앞에 await whenAuthenticated(this)를 두면 로그인 직후 그 자리에서 이어서 로드됩니다. createAuthComponentsPlugin()을 쓰는 앱은 파일을 고치지 않아도 자동으로 이 헬퍼를 쓸 수 있습니다.

다운로드 버튼이 실제로는 다운로드하지 않던 문제 ​

미리보기 창의 「다운로드」와 「새 탭에서 열기」 두 버튼이 같은 함수 previewOpenTab()을 호출하고 있었습니다. 이 함수는 window.open(url, '_blank')으로 새 탭을 여는 것뿐이라, 다운로드 버튼을 눌러도 파일이 저장되지 않고 새 탭만 열렸습니다.

url은 스토리지 서비스가 발급하는 임시 접근 주소(presigned URL)로, 드라이브 앱과는 다른 오리진(로컬은 API 서버, 운영은 S3)을 가리킵니다. <a download> 속성을 이 주소에 직접 걸어도 크롬은 다른 오리진 링크의 download 속성을 조용히 무시하고 새 탭으로 열어버립니다(직접 재현해 확인).

변경 전

변경 후

downloadFile()을 새로 만들어 fetch로 파일 바이트를 직접 받은 뒤 URL.createObjectURL(blob)로 브라우저 메모리 안의 주소(blob: URL)를 만들고 그 주소에 <a download>를 겁니다. blob: URL은 항상 같은 오리진 취급이라 download 속성이 확실히 동작합니다. 실제 클릭으로 download 이벤트가 발생하는 것을 확인했습니다.

엑셀·한글(hwp)처럼 브라우저에 내장 뷰어가 없는 형식은 「새 탭에서 열기」를 눌러도 브라우저가 보여줄 방법이 없어 다운로드로 대체될 뿐입니다(모든 사이트에 공통인 브라우저 동작이라 코드로 바꿀 수 없습니다). 이미 있던 previewKind()(mimeType을 image/pdf/other로 분류하는 함수)를 재사용해 preview.kind !== 'other'일 때만 새 탭 버튼을 보여주도록 숨겼습니다.

아이콘 버튼 두 개의 두께가 서로 다르게 보이던 문제 ​

이번 작업 중 「새 탭에서 열기」·「다운로드」 아이콘 버튼 두 개를 만들며 두께가 서로 다르게 보이는 것을 발견했습니다. 원인을 추적한 결과, 레지스트리의 ic-external-link가 DS 문서(docs/ko/design-system/foundations/iconography.md)가 명시한 표준(24px 좌표계·stroke-width 1.5)을 벗어나 2로 등록돼 있었습니다. ic-download는 표준(1.5)을 따르고 있어서 같은 크기(16px)로 나란히 두면 두께 차이가 났습니다. ic-external-link를 1.5로 정정했습니다(다른 사용처 2곳도 함께 확인, 영향 없음).

이 두 아이콘은 미리보기 창에서만 굵게(stroke 2) 보이도록 사용자 취향을 반영했습니다. 레지스트리 값은 표준(1.5)으로 두고, 호출부에서만 굵기를 조절하는 기존 옵션(getIcon(name, { stroke: 2 }), 2026-08-11에 이미 추가돼 있던 기능)을 얹었습니다. 다른 화면의 같은 아이콘은 영향받지 않습니다.

개념별 변경 사항 ​

좌측 폴더 트리를 admin 「팀 관리」 화면과 통일합니다. apps/drive/src/views/dashboard.html의 좌측 트리를 apps/admin/src/views/tabs/organization.html의 좌측 트리와 같은 규격(행 여백 6/8px·라운드 4px·선택 시 주황 배경)으로 다시 그렸습니다. 계층 연결선(부모-자식 관계를 잇는 세로/꺾임 선)을 추가했는데, 팀 관리 원본은 들여쓰기 단계와 무관하게 선 위치가 고정돼 있어 2단계 이상 깊어지면 선이 행에서 벗어나는 결함이 있었습니다. 드라이브는 --depth CSS 변수로 선 위치를 단계별로 계산해 이 결함을 피했습니다. 폴더별 파일 수 배지도 추가했습니다 — 서버(packages/api/src/routers/team-drive.router.ts)가 폴더에 직접 담긴 파일 수(자신이 볼 수 있는 파일만)를 내려주면, 클라이언트가 하위 폴더까지 합산합니다. 트리 폭은 드래그로 조절할 수 있고(180~480px, localStorage에 저장), 폴더명 검색과 최상위로 돌아가는 「전체」 행도 추가했습니다.

드라이브 상단 검색·좌측 트리 검색, 그리고 팀 관리의 트리·구성원 검색까지 총 5곳에 입력값을 지우는 X 버튼을 넣었습니다. hr-system 시트 검색창에 이미 있던 패턴(입력창을 relative 래퍼로 감싸고, 값이 있을 때만 보이는 X 버튼을 절대 위치로 겹칩니다)을 그대로 따랐습니다. 검색어와 일치하는 부분은 <mark>로 감싸 강조 표시합니다. apps/drive/src/drive-helpers.ts의 highlightMatch()가 이 강조 처리를 하는데, 결과를 x-html로 그대로 화면에 꽂기 때문에 사용자가 올린 파일명이 곧바로 마크업이 되는 것을 막기 위해 강조 전에 반드시 HTML 특수문자를 이스케이프합니다.

미리보기 창 하단 여백이 사라졌던 것을 보정했습니다. 다운로드·새 탭 버튼이 창 하단(footer)에서 상단 헤더로 옮겨가며 footer 자체가 없어졌습니다. 원래 창 하단 여백은 항상 footer의 위쪽 padding(32px)이 만들어 주는 구조라, footer가 사라지자 본문 내용이 창 바닥에 바로 붙었습니다. footer가 없는 다른 모달(memo-history.html)이 이미 같은 문제를 겪어 본문에 padding-bottom: 36px(footer의 원래 아래쪽 padding 값)를 직접 얹어 해결한 전례를 그대로 따랐습니다.

댓글 UX의 뼈대만 추가했습니다(서버 미구현). 미리보기 창 하단에 작성자 아바타·입력창·「댓글 입력」 버튼을 한 줄로 배치했습니다. preview.comments는 항상 빈 배열로 시작하므로 실제로는 항상 「댓글 없음」 상태만 그려집니다. 입력창은 readonly, 버튼은 클릭하면 토스트로 "아직 준비 중"임을 안내합니다. 버튼을 완전한 disabled 상태로 두지 않은 이유는, DS의 disabled 스타일 규칙에 pointer-events: none이 함께 걸려 있어 클릭 자체를 못 잡고 토스트를 띄울 수 없기 때문입니다. 대신 같은 시각 효과(회색 배경·흐린 글자·not-allowed 커서)만 인라인 스타일로 옮겨 붙였습니다 — .ds-btn/.ds-btn--solid가 레이어 없는(unlayered) CSS라 Tailwind 유틸리티 클래스로는 덮어써지지 않았고, !important는 이 저장소의 금지 규칙이라 인라인 스타일로 우회했습니다. 댓글이 있는 경우를 위해 아바타 색도 분기해 뒀습니다 — 내 댓글은 브랜드 그라데이션, 다른 사람 댓글은 메신저(packages/components/html/messenger-content.html)가 이미 쓰는 "다른 사람" 회색(bg-neutral-200/text-neutral-600)입니다.

목록 표의 열 서식을 정리했습니다. 크기·올린 사람·댓글 열은 긴 값이 한 줄로 말줄임 처리되도록 truncate를 추가했고, 수정한 날짜 열은 헤더·값 모두 가운데 정렬로 맞췄습니다. 댓글 열에는 새 댓글 표시 자리(file.hasNewComment)를 마련했습니다 — 서버에 읽음 상태 개념이 아직 없어 항상 숨김 상태입니다.

폴더 만들기 모달에 남아 있던 손모양 마크업을 정리했습니다 ​

상위 폴더 피커가 손으로 그린 패널이었습니다. apps/drive/src/views/modals/folder-form.html의 「상위 폴더」 드롭다운은 bg-white border rounded-lg shadow 래퍼를 직접 그리고 있었습니다. DS 규정(docs/ko/design-system/components/forms/form-controls.md)은 메뉴 패널이 반드시 .ds-menu + .ds-menu__item을 쓰도록 정해 두고 있습니다 — 패널의 배경·테두리·그림자는 .ds-menu가 전담하므로 래퍼에 따로 그리면 안 됩니다. admin 「팀 관리」의 팀 상위 피커(apps/admin/src/views/modals/edit-team.html)가 같은 용도의 이미 규정을 지키는 참조 구현이라, 그 구조를 그대로 따라 .ds-menu/.ds-menu__item/.ds-menu__item--selected로 다시 그렸습니다.

트리거 버튼에 준 .ds-input 클래스가 실제로는 적용되지 않아 테두리가 사라졌습니다. 재구성 과정에서 트리거를 <button class="ds-input ds-input--32">로 만들었는데, 화면에는 테두리가 아예 보이지 않았습니다. design-system.css를 확인해 보니 텍스트필드의 테두리·배경 규칙은 .ds-input이라는 클래스 선택자가 아니라 input:not(...)/textarea:not(...) 태그 선택자로 걸려 있었습니다 — 클래스는 이름만 ds-input일 뿐 실제로 스타일을 부여하는 건 태그 종류입니다. <button> 태그는 이 조건에 애초에 해당하지 않아 어떤 스타일도 받지 못했습니다.

변경 전

변경 후

edit-team.html의 트리거도 같은 이유로 애초에 .ds-input을 쓰지 않고 border rounded text-sm bg-white 유틸리티를 직접 붙이고 있었습니다 — 그 패턴을 그대로 따랐습니다. 검색창(실제 <input> 태그)은 태그 조건을 만족하므로 ds-input--40만으로 정상 동작합니다. 크기는 32에서 40으로 통일했고(트리거·검색창·아래 폴더명/설명 입력까지 세 필드 모두), 검색 결과에는 기존에 상단·좌측 검색에 이미 있던 highlightMatch()를 재사용해 일치 구간을 강조 표시했습니다. 최상위(부모 없음) 옵션의 표기도 조직명("Hereby Demo Corporation") 대신 좌측 트리의 「전체」 행과 같은 문구로 통일했습니다 — 같은 "최상위로 돌아가기" 개념을 화면 두 곳에서 다르게 부르고 있었습니다.

닫기 확인 다이얼로그가 규정 여백을 놓치고 있었습니다 ​

폴더 작성 중 X를 누르면 뜨는 "작성 중인 내용이 있어요" 확인 창도 folder-form.html에 전용 마크업(.ds-modal + pt-6)으로 손으로 그려져 있었습니다. DS 컨펌 다이얼로그 규정은 "상하좌우 28px 일괄"이며 이 수치는 마크업이 아니라 .ds-dialog CSS가 전담하도록 정해져 있습니다. 이 창은 제목 바(header) 없이 본문이 바로 시작하는 형태인데, 수동으로 준 pt-6(24px)은 이 규정값(28px)보다 좁아 제목 위 여백이 눈에 띄게 부족했습니다.

변경 전

변경 후

전용 마크업을 걷어내고 이미 이 저장소 전체가 alert/confirm 대체로 쓰는 공용 askConfirm(@hereby/components, packages/components/html/ds-dialog.html)으로 교체했습니다. 처음에는 title: false(헤더 끔) + tone: "danger"(닫기를 확인 슬롯에 lightor로)로 옮겼는데, 실제 코드베이스의 다른 askConfirm 호출부를 전수로 훑어보니 title: false를 쓰는 곳이 한 곳도 없었고(헤더는 항상 기본값 「확인」으로 켜져 있습니다) 위험 표시도 진짜 파괴적(영구 삭제) 동작에만 씁니다. 그래서 헤더를 되살리고, 버튼도 원래 손그림 마크업의 배치 그대로 「닫기」=왼쪽/취소 슬롯, 「계속 편집」=오른쪽/기본 확인 슬롯(포커스도 여기 남습니다)으로 되돌려 이 저장소의 확인 다이얼로그 관례와 다시 맞췄습니다. 부수 효과로 Esc 처리도 함께 좋아졌습니다 — ds-dialog.html은 Esc 시 stopImmediatePropagation()으로 하위 이벤트 전파를 끊는데, 기존 손그림 마크업에는 이 처리가 없어 확인 창이 뜬 상태에서 Esc를 누르면 폴더 폼 자체의 Esc 핸들러(attemptCloseFolderModal())가 다시 불릴 수 있는 여지가 있었습니다.

한글 입력 중 Esc를 누르면 확인 없이 폴더 모달이 닫히던 문제 ​

작성 중인 내용이 있을 때 Esc를 누르면 컨펌 다이얼로그가 떠야 하는데, "설명" 필드에 한글을 입력하는 도중(조합 중)에 Esc를 누르면 다이얼로그 없이 폴더 모달이 바로 닫히는 문제를 리뷰 중 발견했습니다.

한글·일본어 같은 조합형 문자는 IME(입력기)가 자모를 임시로 모아 두었다가 완성된 글자로 바꿉니다. 이 "조합 중" 상태에서 사용자가 Esc를 누르면 브라우저/IME는 그것을 "조합을 취소하라"는 뜻으로 먼저 처리합니다. 문제는 그 취소용 Esc가 끝나면 같은 keydown 이벤트가 그대로 페이지에도 전달된다는 점입니다 — 결과적으로 IME용 Esc 한 번이 앱 레벨 Esc 핸들러(attemptCloseFolderModal())까지 동시에 실행시켜, 사용자는 "그냥 조합을 취소하려 했을 뿐"인데 모달 전체가 닫혀 버립니다.

이 자체는 새로 만든 코드의 버그가 아니라 이 저장소에 이미 있던 클래스의 문제입니다 — packages/components/src/components/AiAssistant.ts의 onEnter()와 packages/components/html/messenger-content.html이 정확히 같은 이유로 Enter 키에 event.isComposing 가드를 이미 쓰고 있습니다("한글 IME 조합 중 Enter는 무시(조합 확정 Enter로 전송되는 것 방지)"). 폴더 모달의 Esc 핸들러에는 이 가드가 빠져 있었습니다.

folder-form.html의 두 Esc 핸들러(모달 닫기, 상위 폴더 검색창 닫기) 모두에 같은 가드(!$event.isComposing && …)를 추가했습니다. 실제 IME 없이도 재현 가능하도록 isComposing: true를 강제한 합성 keydown 이벤트로 검증했습니다 — 가드 적용 전에는 이 이벤트만으로도 다이얼로그 없이 모달이 닫혔고, 적용 후에는 무시되고 실제(조합 아닌) Esc에서만 정상적으로 다이얼로그가 뜨는 것을 확인했습니다.

알려진 후속 과제 ​

  • 댓글은 UI만 있고 서버 연동이 없습니다. 실제 저장·조회 API가 필요합니다.
  • 같은 IME-Escape 문제가 이 저장소의 다른 @keydown.escape.window 핸들러들(텍스트 입력을 가진 다른 모달들)에도 잠재적으로 있을 수 있습니다. 이번에는 이번 세션에서 손댄 파일(folder-form.html)만 고쳤고, 전체 스윕은 별도 확인 후 진행하는 편이 안전합니다.