[RAPID] PCC: SCR

This commit is contained in:
2026-03-31 15:09:40 +09:00
parent 20b82dc57b
commit b08d337a33
3 changed files with 429 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
# [계획서] 메신저 화면 캡처 기능
> 관련 문서: [맥락노트](./SCR[맥락]-screen-capture.md) | [체크리스트](./SCR[체크]-screen-capture.md)
## 개요
메신저 모달 헤더에 화면 캡처 버튼을 추가하여, 사용자가 드래그로 원하는 영역을 선택하면 해당 영역을 이미지로 캡처하고 메신저 메시지 입력창의 첨부 파일로 자동 추가합니다.
현재 메신저 모달은 화면 캡처 기능이 없어 별도 캡처 도구를 사용한 뒤 파일을 수동으로 첨부해야 합니다.
---
## 현재 동작
### 1. 메신저 헤더에 캡처 버튼 없음
`MessengerModal.tsx` 헤더 영역에는 닫기/최소화 버튼만 있고 캡처 관련 UI가 없음.
### 2. MessageInput의 파일 추가 기능이 외부에 노출되지 않음
`MessageInput.tsx``addFiles` 함수는 컴포넌트 내부에서만 사용되며, `ref`를 통해 외부에서 호출할 수 있는 인터페이스가 없음.
### 3. 캡처 전용 컴포넌트 없음
드래그 선택 영역 표시, `modern-screenshot` 연동, 오버레이 관리 등의 캡처 로직을 처리하는 컴포넌트가 없음.
---
## 변경 후 동작
### 1. 캡처 버튼 추가
`MessengerModal.tsx` 헤더에 Scissors 아이콘 버튼이 추가됨. 클릭 시 캡처 모드로 진입.
### 2. 캡처 모드 진입
- 메신저 모달이 `display: none`으로 숨겨짐
- `ScreenCapture` 컴포넌트가 전체화면 오버레이로 표시됨 (`z-[99999]`, 반투명 어두운 배경, `crosshair` 커서)
### 3. 영역 선택
사용자가 마우스로 드래그하면 선택 영역이 파란 반투명 사각형으로 표시됨.
### 4. 캡처 및 파일 추가
`mouseup``modern-screenshot`으로 해당 DOM 영역을 캡처 → `File` 객체로 변환 → `MessageInput``addFiles`를 ref로 호출하여 `pendingFiles`에 추가.
### 5. 복원
오버레이가 제거되고 메신저 모달이 다시 표시됨.
---
## 시각적 예시
### 캡처 모드 오버레이
```
┌─────────────────────────────────────────────────────────┐
│ (전체 화면, rgba(0,0,0,0.35), crosshair 커서) │
│ │
│ ┌──────────────────────────────┐ │
│ │ (드래그 선택 영역) │ │
│ │ 파란 반투명 rect │ │
│ │ border: 2px solid #3b82f6 │ │
│ │ background: rgba(59,130, │ │
│ │ 246, 0.2) │ │
│ └──────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
### 헤더 버튼 위치
```
┌─ 메신저 ─────────────────────────────────── [✂] [_] [X] ┐
│ │
```
---
## 아키텍처
### 데이터 흐름
```mermaid
flowchart TD
A["사용자: 헤더 Scissors 버튼 클릭"] --> B["MessengerModal\n모달 숨김 (display:none)"]
B --> C["ScreenCapture 오버레이 표시\nz-[99999], 반투명 배경"]
C --> D["사용자: 마우스 드래그로 영역 선택\n파란 반투명 rect 표시"]
D --> E["mouseup: modern-screenshot\n해당 DOM 영역 캡처"]
E --> F["Blob → File 객체 변환"]
F --> G["messageInputRef.current.addFiles(file)\nMessageInput pendingFiles에 추가"]
G --> H["오버레이 제거\n메신저 모달 복원"]
```
### 컴포넌트 관계
```mermaid
graph LR
subgraph modal ["MessengerModal.tsx"]
H["헤더 캡처 버튼"]
SHOW["모달 표시/숨김 제어"]
end
subgraph capture ["ScreenCapture.tsx (신규)"]
OV["전체화면 오버레이"]
SEL["드래그 선택 rect"]
CAP["modern-screenshot 캡처"]
end
subgraph input ["MessageInput.tsx"]
AF["addFiles (ref 노출)"]
PF["pendingFiles 상태"]
end
H -->|"캡처 모드 진입"| SHOW
SHOW -->|"onCapture 콜백"| OV
CAP -->|"File 객체"| AF
AF --> PF
```
---
## 변경 대상 파일
| 파일 | 수정 내용 | 수정 규모 |
|------|----------|----------|
| `package.json` | `modern-screenshot` 패키지 추가 | 1줄 |
| `components/messenger/MessengerModal.tsx` | 헤더에 Scissors 버튼 추가, 캡처 모드 상태 관리, 모달 숨김/복원 처리 | ~30줄 |
| `components/messenger/ScreenCapture.tsx` | **신규** - 전체화면 오버레이, 드래그 선택, modern-screenshot 캡처, File 객체 반환 | ~150줄 |
| `components/messenger/MessageInput.tsx` | `addFiles``useImperativeHandle`로 외부 ref에 노출 | ~15줄 |
### 변경하지 않는 파일
- `contexts/MessengerContext.tsx` - 캡처 트리거를 Context로 공유할 필요 없음. `MessengerModal` 내부에서 `ref`로 직접 연결하는 것으로 충분
- 백엔드 전체 - 파일 첨부는 기존 메시지 전송 흐름을 그대로 사용
---
## 코드 설계
### 1. MessageInput ref 인터페이스 (MessageInput.tsx)
```typescript
export interface MessageInputHandle {
addFiles: (files: File[]) => void;
}
const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(
(props, ref) => {
useImperativeHandle(ref, () => ({
addFiles: (files: File[]) => {
setPendingFiles((prev) => [...prev, ...files]);
},
}));
// ...
}
);
```
### 2. ScreenCapture 컴포넌트 인터페이스 (ScreenCapture.tsx)
```typescript
interface ScreenCaptureProps {
onCapture: (file: File) => void;
onCancel: () => void;
}
```
- `mousedown`: 시작 좌표 저장
- `mousemove`: 선택 rect 업데이트
- `mouseup`: `modern-screenshot`으로 `document.elementFromPoint` 기반 영역 캡처
- `Escape` 키: 취소
### 3. MessengerModal 캡처 모드 (MessengerModal.tsx)
```typescript
const messageInputRef = useRef<MessageInputHandle>(null);
const [isCaptureMode, setIsCaptureMode] = useState(false);
// 모달 숨김
<div style={{ display: isCaptureMode ? 'none' : undefined }}>
{/* 기존 모달 내용 */}
<MessageInput ref={messageInputRef} ... />
</div>
// 캡처 오버레이
{isCaptureMode && (
<ScreenCapture
onCapture={(file) => {
messageInputRef.current?.addFiles([file]);
setIsCaptureMode(false);
}}
onCancel={() => setIsCaptureMode(false)}
/>
)}
```
### 4. modern-screenshot 캡처 (ScreenCapture.tsx)
```typescript
import { toBlob } from 'modern-screenshot';
const handleMouseUp = async () => {
const el = document.elementFromPoint(centerX, centerY) as HTMLElement;
const blob = await toBlob(el, {
width: selectionWidth,
height: selectionHeight,
// clip 옵션으로 선택 영역만 캡처
});
const file = new File([blob], `capture-${Date.now()}.png`, {
type: 'image/png',
});
onCapture(file);
};
```
---
## 설계 원칙
- `MessengerContext` 변경 없음 - 캡처는 모달 내부 관심사이므로 Context 전파 불필요
- 캡처 오버레이는 `portal``document.body`에 렌더링하여 기존 레이아웃 z-index 충돌 방지
- `modern-screenshot`은 DOM 기반 캡처이므로 브라우저 권한(Screen Capture API) 불필요
- `forwardRef` + `useImperativeHandle` 패턴은 프로젝트 기존 패턴과 동일하게 적용

View File

@@ -0,0 +1,129 @@
# [맥락노트] 메신저 화면 캡처 기능
> 관련 문서: [계획서](./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 등)는 외부에서 직접 접근 불가
```

View File

@@ -0,0 +1,76 @@
# [체크리스트] 메신저 화면 캡처 기능
> 관련 문서: [계획서](./SCR[계획]-screen-capture.md) | [맥락노트](./SCR[맥락]-screen-capture.md)
---
## 공정 상태
- 전체 진행률: **0%** (구현 전)
- 현재 단계: 대기
---
## 구현 체크리스트
### 0단계: 패키지 설치
- [ ] `package.json``modern-screenshot` 추가 및 `npm install` 실행
- [ ] `import { toBlob } from 'modern-screenshot'` 정상 동작 확인
### 1단계: MessageInput ref 인터페이스 추가
- [ ] `MessageInputHandle` 인터페이스 정의 (`addFiles: (files: File[]) => void`)
- [ ] `MessageInput``forwardRef<MessageInputHandle, MessageInputProps>`로 변경
- [ ] `useImperativeHandle``addFiles` 노출 (`setPendingFiles` 호출)
- [ ] 기존 `MessageInput` 사용처에서 타입 에러 없음 확인
### 2단계: ScreenCapture 컴포넌트 신규 생성
- [ ] `ScreenCapture.tsx` 파일 생성 (`components/messenger/`)
- [ ] `createPortal``document.body`에 오버레이 렌더링
- [ ] 오버레이 스타일 적용: `fixed inset-0 z-[99999] bg-black/35 cursor-crosshair`
- [ ] `mousedown`: 시작 좌표(`startX`, `startY`) 상태 저장
- [ ] `mousemove`: 선택 rect 좌표/크기 실시간 업데이트
- [ ] 선택 rect UI: `absolute border-2 border-blue-500 bg-blue-500/20`
- [ ] `mouseup`: `toBlob` 호출로 캡처 실행
- [ ] `File` 객체 생성 (`capture-${Date.now()}.png`, `image/png`)
- [ ] `onCapture(file)` 콜백 호출
- [ ] `Escape` 키 이벤트 리스너 등록 → `onCancel` 호출
- [ ] 컴포넌트 언마운트 시 이벤트 리스너 정리 (`useEffect` cleanup)
### 3단계: MessengerModal 캡처 모드 연결
- [ ] `isCaptureMode` 상태 추가 (`useState(false)`)
- [ ] `messageInputRef` 생성 (`useRef<MessageInputHandle>(null)`)
- [ ] `MessageInput``ref={messageInputRef}` 연결
- [ ] 헤더에 Scissors 아이콘 버튼 추가 (lucide-react `Scissors`)
- [ ] 버튼 클릭 시 `setIsCaptureMode(true)` 호출
- [ ] 모달 루트 div에 `style={{ display: isCaptureMode ? 'none' : undefined }}` 적용
- [ ] `isCaptureMode === true`일 때 `ScreenCapture` 렌더링
- [ ] `onCapture`: `messageInputRef.current?.addFiles([file])``setIsCaptureMode(false)`
- [ ] `onCancel`: `setIsCaptureMode(false)`
### 4단계: 검증
- [ ] 캡처 버튼 클릭 시 모달이 숨겨지고 오버레이가 표시되는지 확인
- [ ] 드래그 중 파란 반투명 rect가 정확히 그려지는지 확인
- [ ] 캡처 완료 후 모달이 복원되고 이미지가 MessageInput 첨부 영역에 추가되는지 확인
- [ ] 캡처 전 기존에 첨부한 파일이 캡처 후에도 유지되는지 확인 (display:none 상태 보존)
- [ ] Escape 키로 취소 시 모달이 정상 복원되는지 확인
- [ ] 캡처된 이미지를 메시지로 전송했을 때 수신측에서 정상 표시되는지 확인
- [ ] 고해상도 디스플레이(Retina)에서 캡처 이미지 해상도가 충분한지 확인
### 5단계: 정리
- [ ] 린트 에러 없음 확인
- [ ] `console.log` 등 디버그 코드 제거 확인
- [ ] 이 체크리스트 완료 표시 업데이트
---
## 변경 이력
| 날짜 | 내용 |
|------|------|
| 2026-03-31 | 계획서, 맥락노트, 체크리스트 작성 완료 |