130 lines
6.1 KiB
Markdown
130 lines
6.1 KiB
Markdown
# [맥락노트] 메신저 화면 캡처 기능
|
|
|
|
> 관련 문서: [계획서](./SCR[계획]-screen-capture.md) | [체크리스트](./SCR[체크]-screen-capture.md)
|
|
|
|
---
|
|
|
|
## 왜 이 작업을 하는가
|
|
|
|
- 메신저로 업무 화면을 공유할 때 외부 캡처 도구(OS 단축키 등)를 사용하면 파일을 저장 후 다시 첨부해야 하는 번거로움이 있음
|
|
- 메신저 모달 안에서 바로 캡처 → 첨부까지 원스톱으로 처리할 수 있어야 업무 효율이 높아짐
|
|
- 특히 ERP 화면(테이블, 폼 등)을 빠르게 캡처해서 담당자에게 공유하는 시나리오가 자주 발생함
|
|
|
|
---
|
|
|
|
## 핵심 결정 사항과 근거
|
|
|
|
### 1. MessengerContext 변경 없음
|
|
|
|
- **결정**: 캡처 트리거를 Context로 공유하지 않고 `MessengerModal` 내부 상태(`useState`)로 관리
|
|
- **근거**: 캡처 기능은 모달 UI의 관심사. 다른 컴포넌트가 캡처 상태를 구독할 이유가 없음. Context에 넣으면 불필요한 리렌더가 발생하고 범위가 넓어짐
|
|
- **대안 검토**: Context에 `startCapture` 액션 추가 → 기각 (단일 모달 내부 동작에 Context 오버엔지니어링)
|
|
|
|
### 2. 모달 숨김을 display:none으로 처리
|
|
|
|
- **결정**: 캡처 모드 진입 시 모달 루트 div에 `style={{ display: 'none' }}` 적용
|
|
- **근거**: `unmount`하면 `MessageInput`의 `pendingFiles` 상태가 초기화됨. `display:none`은 DOM을 유지하면서 숨기므로 상태 보존
|
|
- **대안 검토**: `visibility: hidden` → 공간을 차지하여 캡처 영역을 가릴 수 있으므로 기각
|
|
|
|
### 3. ScreenCapture를 Portal로 document.body에 렌더링
|
|
|
|
- **결정**: `createPortal(overlay, document.body)` 사용
|
|
- **근거**: 메신저 모달의 `z-index` 스택 컨텍스트에 종속되면 `z-[99999]`가 실제로 최상위가 되지 않을 수 있음. Portal로 body에 직접 붙이면 stacking context 이슈를 원천 차단
|
|
- **대안 검토**: 모달 내부에 렌더링 + 높은 z-index → 부모 stacking context에 따라 동작이 불안정하므로 기각
|
|
|
|
### 4. modern-screenshot 선택
|
|
|
|
- **결정**: `modern-screenshot` 라이브러리의 `toBlob` 사용
|
|
- **근거**: `html2canvas`보다 최신이고 Shadow DOM, CSS 변수 지원이 우수함. ERP 화면에 CSS 변수와 Tailwind가 많이 사용되므로 렌더링 정확도가 중요
|
|
- **대안 검토**: `html2canvas` → Shadow DOM/CSS 변수 지원 미흡으로 기각. Screen Capture API (`getDisplayMedia`) → 브라우저 권한 팝업이 UX를 해치므로 기각
|
|
|
|
### 5. addFiles를 useImperativeHandle로 노출
|
|
|
|
- **결정**: `MessageInput`을 `forwardRef`로 변경하고 `addFiles`를 `useImperativeHandle`로 외부에 노출
|
|
- **근거**: `pendingFiles` 상태를 `MessageInput` 외부로 끌어올리면 메시지 입력 관련 로직이 분산됨. ref 인터페이스로 최소한의 표면만 노출하는 것이 응집도를 유지하는 방법
|
|
- **대안 검토**: `pendingFiles`를 `MessengerModal`로 끌어올림 → `MessageInput` 내부 로직 전체가 영향을 받아 범위가 넓어지므로 기각
|
|
|
|
### 6. 캡처 대상 DOM 결정 방식
|
|
|
|
- **결정**: 선택 영역의 중심점 좌표로 `document.elementFromPoint`를 사용하여 캡처 대상 element를 찾고, `toBlob`에 clip 옵션으로 정확한 영역만 추출
|
|
- **근거**: 선택 영역이 여러 요소에 걸쳐 있을 수 있으므로 `document.body` 전체를 캡처 후 crop하는 방식이 더 안정적
|
|
- **대안 검토**: `document.body` 전체 캡처 후 Canvas로 crop → 전체 페이지 캡처는 스크롤 위치, iframe 등 변수가 많아 복잡도 증가. `toBlob`의 clip 옵션 활용이 심플함
|
|
|
|
### 7. Escape 키로 취소
|
|
|
|
- **결정**: 오버레이 활성 중 `Escape` 키 입력 시 캡처 취소 후 모달 복원
|
|
- **근거**: 사용자가 실수로 캡처 모드에 진입했을 때 직관적인 탈출 수단이 필요함. 프로젝트 내 다른 모달들도 `Escape`로 닫히는 패턴을 따름
|
|
|
|
---
|
|
|
|
## 관련 파일 위치
|
|
|
|
| 구분 | 파일 경로 | 설명 |
|
|
|------|----------|------|
|
|
| 수정 대상 | `frontend/components/messenger/MessengerModal.tsx` | 헤더 버튼, 캡처 모드 상태, 모달 숨김/복원 |
|
|
| 신규 생성 | `frontend/components/messenger/ScreenCapture.tsx` | 오버레이, 드래그 선택, 캡처 실행 |
|
|
| 수정 대상 | `frontend/components/messenger/MessageInput.tsx` | forwardRef + useImperativeHandle로 addFiles 노출 |
|
|
| 수정 대상 | `frontend/package.json` | modern-screenshot 의존성 추가 |
|
|
| 변경 없음 | `frontend/contexts/MessengerContext.tsx` | 캡처 상태 공유 불필요로 변경 없음 |
|
|
|
|
---
|
|
|
|
## 기술 참고
|
|
|
|
### 캡처 모드 상태 전환
|
|
|
|
```
|
|
초기 상태:
|
|
모달 표시 (display: block)
|
|
isCaptureMode = false
|
|
ScreenCapture 미렌더링
|
|
|
|
캡처 버튼 클릭:
|
|
isCaptureMode = true
|
|
→ 모달 div: display: none
|
|
→ ScreenCapture: Portal로 body에 렌더링
|
|
|
|
영역 선택 완료 (mouseup):
|
|
modern-screenshot toBlob 실행
|
|
→ File 객체 생성
|
|
→ messageInputRef.current.addFiles([file])
|
|
→ isCaptureMode = false
|
|
→ 모달 복원 (display: block)
|
|
|
|
취소 (Escape 또는 onCancel):
|
|
isCaptureMode = false
|
|
→ 모달 복원
|
|
```
|
|
|
|
### modern-screenshot toBlob 옵션
|
|
|
|
```typescript
|
|
await toBlob(document.body, {
|
|
x: rect.x, // 선택 영역 좌측 상단 x (뷰포트 기준)
|
|
y: rect.y, // 선택 영역 좌측 상단 y (뷰포트 기준)
|
|
width: rect.width,
|
|
height: rect.height,
|
|
scale: window.devicePixelRatio, // 고해상도 디스플레이 대응
|
|
});
|
|
```
|
|
|
|
### pendingFiles 상태 보존 이유
|
|
|
|
```
|
|
캡처 전 사용자가 이미 파일을 첨부했을 수 있음
|
|
→ MessageInput unmount 시 pendingFiles 초기화
|
|
→ display:none으로 DOM/상태 유지
|
|
→ 캡처 완료 후 기존 파일 + 캡처 이미지가 함께 첨부됨
|
|
```
|
|
|
|
### forwardRef 패턴 적용 범위
|
|
|
|
```
|
|
변경 전: const MessageInput = (props: MessageInputProps) => { ... }
|
|
변경 후: const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>((props, ref) => { ... })
|
|
|
|
외부 인터페이스:
|
|
messageInputRef.current.addFiles(files) ← 이것만 노출
|
|
내부 상태(pendingFiles, text 등)는 외부에서 직접 접근 불가
|
|
```
|