[RAPID] PCC: SCR
This commit is contained in:
224
frontend/docs/yc/SCR[계획]-screen-capture.md
Normal file
224
frontend/docs/yc/SCR[계획]-screen-capture.md
Normal 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` 패턴은 프로젝트 기존 패턴과 동일하게 적용
|
||||
129
frontend/docs/yc/SCR[맥락]-screen-capture.md
Normal file
129
frontend/docs/yc/SCR[맥락]-screen-capture.md
Normal 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 등)는 외부에서 직접 접근 불가
|
||||
```
|
||||
76
frontend/docs/yc/SCR[체크]-screen-capture.md
Normal file
76
frontend/docs/yc/SCR[체크]-screen-capture.md
Normal 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 | 계획서, 맥락노트, 체크리스트 작성 완료 |
|
||||
Reference in New Issue
Block a user