Skip to content

Conversation

@ohah
Copy link
Owner

@ohah ohah commented Oct 19, 2025

🖼️ PR: 이미지 미리보기 및 클립보드 복사 기능 구현

📋 요약

이 PR은 네트워크 요청/응답에서 이미지 파일을 직접 미리보기할 수 있는 기능과 이미지 클립보드 복사 기능을 구현합니다.

✨ 주요 기능

1. 이미지 미리보기 기능

  • 바이너리 이미지 데이터를 UI에서 직접 표시
  • 편집 모드에서도 이미지 미리보기 유지 (편집 불가능)
  • 풀스크린 모드 및 다운로드 기능
  • 다양한 이미지 형식 지원 (PNG, JPEG, GIF, WebP, SVG)

2. Web Worker 기반 성능 최적화

  • Base64 변환을 Web Worker에서 처리 (메인 스레드 블로킹 방지)
  • 378ms 지연 시간 해결
  • 병렬 처리 지원 (여러 이미지 동시 처리)
  • Fallback 메커니즘 (Worker 실패 시 기존 방식 사용)

4. 데이터 타입 감지 개선

  • Rust 백엔드에서 정확한 이미지/비디오 구분
  • MP4 브랜드 식별자 확인으로 정확한 비디오 감지
  • 추가 이미지 형식 지원 (BMP, ICO, TIFF)

🔧 기술적 개선사항

백엔드 (Rust)

  • crates/proxy_v2_models/src/data_type.rs: 데이터 타입 감지 로직 개선
  • tauri-plugin-clipboard-manager: 클립보드 매니저 플러그인 추가

프론트엔드 (TypeScript/React)

  • ImagePreview 컴포넌트: 새로운 이미지 미리보기 컴포넌트
  • useBase64Worker Hook: Web Worker 관리
  • base64-worker.ts: Base64 변환 Web Worker
  • TransactionBody/TransactionResponse: 이미지 미리보기 및 클립보드 복사 통합

설정 및 빌드

  • Rspack Worker 지원 설정
  • Tauri 클립보드 매니저 권한 설정
  • React Scan 개발 환경 설정

🚀 사용자 경험 개선

  • 직관적인 이미지 미리보기: 바이너리 데이터를 실제 이미지로 표시
  • 빠른 응답성: Web Worker로 메인 스레드 블로킹 제거
  • 편리한 클립보드 복사: 이미지를 클립보드에 직접 복사
  • 안정적인 Fallback: 실패 시 자동 다운로드로 대안 제공

🔍 추가 개선 사항

  • UI 스레드 최적화

@ohah ohah requested review from Copilot, jinjoo-dev, newminkyung and wontaezia and removed request for Copilot October 19, 2025 17:47
@ohah ohah merged commit b843671 into master Oct 19, 2025
2 checks passed
@ohah ohah deleted the feat/image-preview branch October 19, 2025 17:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements image preview and clipboard copy functionality for network request/response data, enabling direct visualization of binary image data in the UI. The implementation includes Web Worker-based performance optimization to prevent main thread blocking during Base64 conversion.

Key Changes

  • Web Worker implementation for Base64 conversion to prevent UI blocking
  • Image preview component with fullscreen mode and download functionality
  • Enhanced data type detection for better image/video format recognition

Reviewed Changes

Copilot reviewed 14 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tauri-ui/src/workers/base64-worker.ts New Web Worker for non-blocking Base64 conversion
tauri-ui/src/hooks/use-base64-worker.ts Hook for managing Web Worker lifecycle and tasks
tauri-ui/src/features/transaction-details/ui/image-preview.tsx Image preview component with fullscreen and download features
tauri-ui/src/features/transaction-details/ui/transaction-response.tsx Integrated image preview and clipboard copy for responses
tauri-ui/src/features/transaction-details/ui/transaction-body.tsx Integrated image preview and clipboard copy for requests
tauri-ui/src/features/transaction-details/lib/utils.ts Utility functions for Base64 conversion and image data URL creation
crates/proxy_v2_models/src/data_type.rs Enhanced image/video detection with additional format support
tauri-ui/src-tauri/Cargo.toml Added clipboard manager plugin dependency
tauri-ui/src-tauri/capabilities/default.json Added clipboard permissions
tauri-ui/rspack.config.ts Worker support configuration
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +56 to +74
function uint8ArrayToBase64(data: Uint8Array | number[]): string {
if (!data || data.length === 0) {
return '';
}

const uint8Array = data instanceof Uint8Array ? data : new Uint8Array(data);

// 청크 단위로 처리하여 메모리 효율성 향상
const chunkSize = 8192; // 8KB 청크
let result = '';

for (let i = 0; i < uint8Array.length; i += chunkSize) {
const chunk = uint8Array.slice(i, i + chunkSize);
const binary = Array.from(chunk, (byte) => String.fromCharCode(byte)).join('');
result += btoa(binary);
}

return result;
}
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The chunked Base64 encoding is incorrect. Each chunk is being encoded separately with btoa(), but Base64 encoding requires the entire data to be encoded as one unit. This will produce invalid Base64 output. Use a streaming approach or encode the entire binary string at once.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +57
export const uint8ArrayToBase64 = (data: Uint8Array | number[]): string => {
if (!data || data.length === 0) {
return '';
}

try {
// 일반 배열인 경우 Uint8Array로 변환
const uint8Array = data instanceof Uint8Array ? data : new Uint8Array(data);

// Uint8Array를 문자열로 변환한 후 Base64 인코딩
let binary = '';
for (let i = 0; i < uint8Array.length; i++) {
binary += String.fromCharCode(uint8Array[i]);
}
return btoa(binary);
} catch (error) {
console.error('Base64 인코딩 실패:', error);
return '';
}
};
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function contains duplicated Base64 encoding logic that exists in the worker file. Consider extracting this to a shared utility or ensure consistency between implementations.

Copilot uses AI. Check for mistakes.

const getImageFileExtension = (dataType: string): string => {
// MIME 타입에서 확장자 추출하는 간단한 함수
return 'png'; // 기본값
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function getImageFileExtension only returns 'png' and doesn't actually extract extensions from MIME types as the comment suggests. This could lead to incorrect file extensions when downloading images.

Suggested change
return 'png'; // 기본값
// e.g., "image/png" -> "png", "image/jpeg" -> "jpg", "image/gif" -> "gif"
if (!dataType) return 'png';
const mimeToExt: Record<string, string> = {
'image/png': 'png',
'image/jpeg': 'jpg',
'image/jpg': 'jpg',
'image/gif': 'gif',
'image/webp': 'webp',
'image/bmp': 'bmp',
'image/svg+xml': 'svg',
'image/tiff': 'tiff',
};
if (mimeToExt[dataType]) return mimeToExt[dataType];
// Fallback: try to extract after '/' and handle '+xml'
const parts = dataType.split('/');
if (parts.length === 2) {
let ext = parts[1];
// Remove '+xml' if present (e.g., 'svg+xml')
ext = ext.replace(/\+xml$/, '');
return ext;
}
return 'png';

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +88
const getImageFileExtension = (dataType: string): string => {
// MIME 타입에서 확장자 추출하는 간단한 함수
return 'png'; // 기본값
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate implementation of getImageFileExtension that only returns 'png'. This logic is duplicated across multiple files and should be extracted to a shared utility function with proper MIME type to extension mapping.

Suggested change
const getImageFileExtension = (dataType: string): string => {
// MIME 타입에서 확장자 추출하는 간단한 함수
return 'png'; // 기본값
// Utility function to map MIME type to file extension
const getImageFileExtension = (dataType: string): string => {
const mimeToExt: Record<string, string> = {
'image/png': 'png',
'image/jpeg': 'jpg',
'image/jpg': 'jpg',
'image/gif': 'gif',
'image/webp': 'webp',
'image/bmp': 'bmp',
'image/svg+xml': 'svg',
};
return mimeToExt[dataType] || 'png'; // 기본값

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants