Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/features/event/hooks/usePresignedUrlHook.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { PresignedUrlRequest, PresignedUrlResponse } from '../model/presignedUrl';
import { axiosClient } from '../../../shared/types/api/http-client';

import axios from 'axios';
import { ApiResponse } from '../../../shared/types/api/apiResponse';
import { convertImageToWebP } from '../../../shared/lib/convertImageToWebP';

const getPresignedUrl = async (dto: PresignedUrlRequest) => {
try {
Expand All @@ -24,7 +24,7 @@ export const putS3Image = async ({ url, file }: { url: string; file: File }) =>
console.log('업로드할 URL:', url);
await axios.put(url, file, {
headers: {
'Content-Type': file.type,
'Content-Type': 'image/webp',
},
});
} catch (error) {
Expand All @@ -35,8 +35,10 @@ export const putS3Image = async ({ url, file }: { url: string; file: File }) =>
};

export const uploadFile = async (file: File) => {
const { name } = file;
const presignedUrlResponse = await getPresignedUrl({ fileName: name });
const webFile = await convertImageToWebP(file);
const fileName = webFile.name;

const presignedUrlResponse = await getPresignedUrl({ fileName });

if (!presignedUrlResponse) {
throw new Error('Failed to get presigned url');
Expand All @@ -45,7 +47,7 @@ export const uploadFile = async (file: File) => {
const url = presignedUrlResponse;
console.log('Presigned URL:', url);

await putS3Image({ url, file });
await putS3Image({ url, file: webFile });

// S3 URL에서 presigned URL 파라미터를 제거하고 기본 URL 반환
return url.split('?')[0];
Expand Down
4 changes: 3 additions & 1 deletion src/shared/hooks/useImageUpload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRef, useState, useCallback, useEffect } from 'react';
import { uploadFile } from '../../features/event/hooks/usePresignedUrlHook';
import { convertImageToWebP } from '../lib/convertImageToWebP';
export const DEFAULT_BASIC_PROFILE = 'https://gotogetherbucket.s3.ap-northeast-2.amazonaws.com/default.png';

const useImageUpload = ({
Expand Down Expand Up @@ -41,7 +42,8 @@ const useImageUpload = ({
if (!validateFile(file)) return;

try {
const imageUrl = await uploadFile(file);
const webpFile = await convertImageToWebP(file);
const imageUrl = await uploadFile(webpFile);
setPreviewUrl(imageUrl);
onSuccess?.(imageUrl);
} catch (error) {
Expand Down
36 changes: 36 additions & 0 deletions src/shared/lib/convertImageToWebP.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export const convertImageToWebP = (file: File): Promise<File> => {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();

reader.onload = () => {
if (typeof reader.result === 'string') {
img.src = reader.result;
}
};

img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;

const ctx = canvas.getContext('2d');
if (!ctx) return reject(new Error('Canvas context error'));

ctx.drawImage(img, 0, 0);

canvas.toBlob(blob => {
if (!blob) return reject(new Error('WebP 변환 실패'));
const webpFile = new File([blob], file.name.replace(/\.\w+$/, '.webp'), {
type: 'image/webp',
});
resolve(webpFile);
}, 'image/webp');
};

img.onerror = reject;
reader.onerror = reject;

reader.readAsDataURL(file);
});
};
31 changes: 31 additions & 0 deletions src/shared/lib/migrateJpgToWebp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { uploadFile } from '../../features/event/hooks/usePresignedUrlHook';
import { convertImageToWebP } from './convertImageToWebP';

const oldJpgUrls: string[] = [
// ... 여기에 변환할 이미지 URL들
];

export const runImageMigration = async () => {
for (const jpgUrl of oldJpgUrls) {
try {
const response = await fetch(jpgUrl);
const blob = await response.blob();

const file = new File([blob], extractFileName(jpgUrl), { type: blob.type });

const webpFile = await convertImageToWebP(file);
const webpUrl = await uploadFile(webpFile);

console.log(`✅ ${jpgUrl} → ${webpUrl}`);
} catch (error) {
console.error(`❌ 변환 실패: ${jpgUrl}`, error);
}
}

console.log('✅ 전체 마이그레이션 완료');
};
Comment on lines +8 to +26
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

마이그레이션 로직 개선 제안

기본 구조는 좋지만 다음 개선사항들을 고려해보세요:

  1. 동시 요청 제한: 많은 이미지를 한번에 처리하면 메모리 부족이나 서버 부하가 발생할 수 있습니다.
  2. 네트워크 타임아웃: fetch 요청에 타임아웃을 설정하세요.
  3. 재시도 로직: 네트워크 오류 시 재시도 메커니즘을 추가하세요.
export const runImageMigration = async () => {
+  const BATCH_SIZE = 5; // 동시 처리할 이미지 수 제한
+  
+  for (let i = 0; i < oldJpgUrls.length; i += BATCH_SIZE) {
+    const batch = oldJpgUrls.slice(i, i + BATCH_SIZE);
+    const promises = batch.map(async (jpgUrl) => {
-  for (const jpgUrl of oldJpgUrls) {
      try {
-        const response = await fetch(jpgUrl);
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 30000); // 30초 타임아웃
+        
+        const response = await fetch(jpgUrl, { 
+          signal: controller.signal 
+        });
+        clearTimeout(timeoutId);
+        
        const blob = await response.blob();
        // ... 나머지 로직
      } catch (error) {
        console.error(`❌ 변환 실패: ${jpgUrl}`, error);
      }
+    });
+    
+    await Promise.allSettled(promises);
+    
+    // 배치 간 딜레이
+    if (i + BATCH_SIZE < oldJpgUrls.length) {
+      await new Promise(resolve => setTimeout(resolve, 1000));
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const runImageMigration = async () => {
for (const jpgUrl of oldJpgUrls) {
try {
const response = await fetch(jpgUrl);
const blob = await response.blob();
const file = new File([blob], extractFileName(jpgUrl), { type: blob.type });
const webpFile = await convertImageToWebP(file);
const webpUrl = await uploadFile(webpFile);
console.log(`✅ ${jpgUrl}${webpUrl}`);
} catch (error) {
console.error(`❌ 변환 실패: ${jpgUrl}`, error);
}
}
console.log('✅ 전체 마이그레이션 완료');
};
export const runImageMigration = async () => {
const BATCH_SIZE = 5; // 동시 처리할 이미지 수 제한
for (let i = 0; i < oldJpgUrls.length; i += BATCH_SIZE) {
const batch = oldJpgUrls.slice(i, i + BATCH_SIZE);
const promises = batch.map(async (jpgUrl) => {
try {
// 네트워크 타임아웃 설정
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30초 타임아웃
const response = await fetch(jpgUrl, { signal: controller.signal });
clearTimeout(timeoutId);
const blob = await response.blob();
const file = new File([blob], extractFileName(jpgUrl), { type: blob.type });
const webpFile = await convertImageToWebP(file);
const webpUrl = await uploadFile(webpFile);
console.log(`✅ ${jpgUrl}${webpUrl}`);
} catch (error) {
console.error(`❌ 변환 실패: ${jpgUrl}`, error);
}
});
// 현재 배치의 모든 작업이 끝날 때까지 대기
await Promise.allSettled(promises);
// 배치 간 딜레이 (옵션)
if (i + BATCH_SIZE < oldJpgUrls.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
console.log('✅ 전체 마이그레이션 완료');
};
🤖 Prompt for AI Agents
In src/shared/lib/migrateJpgToWebp.ts around lines 8 to 26, the image migration
runs all fetch and conversion requests sequentially without concurrency control,
lacks timeout handling on fetch calls, and does not retry on network failures.
To fix this, implement a concurrency limit to process a limited number of images
simultaneously, add a timeout mechanism to the fetch requests to avoid hanging,
and include a retry logic that attempts the fetch and conversion a few times
before failing, to improve robustness and resource management.


const extractFileName = (url: string) => {
const baseName = url.split('/').pop()?.split('?')[0] ?? 'unknown.jpg';
return baseName.replace(/\.(jpg|jpeg)$/i, '.webp');
};
7 changes: 4 additions & 3 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import react from '@vitejs/plugin-react-swc';
import path from 'path';
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
import fs from 'fs';
import { visualizer } from 'rollup-plugin-visualizer';
// import fs from 'fs';

// https://vite.dev/config/
export default defineConfig(({ mode }) => {
Expand Down Expand Up @@ -35,10 +35,11 @@ export default defineConfig(({ mode }) => {
},
server: {
host: true,
https: {
// 로컬 실행을 위해 일시적으로 https 로컬 설정 주석처리
/* https: {
key: fs.readFileSync('gotogether.io.kr+3-key.pem'),
cert: fs.readFileSync('gotogether.io.kr+3.pem'),
},
}, */
allowedHosts: ['gotogether.io.kr'],
proxy: {
'/api/v1': {
Expand Down