Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 12 additions & 5 deletions src/features/event/ui/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import FileUploadImage from '../../../../public/assets/event-manage/creation/FileUpload.svg';
import { FunnelState, useFunnelState } from '../model/FunnelContext';
import { FunnelState } from '../model/FunnelContext';
import useImageUpload from '../../../shared/hooks/useImageUpload';
import { useEffect } from 'react';

interface FileUploadProps {
value?: string;
onChange?: (url: string) => void;
eventState?: FunnelState['eventState'];
setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>;
useDefaultImage?: boolean;
onValidationChange?: (isValid: boolean) => void;
}

const FileUpload = ({ onChange, setEventState, useDefaultImage, onValidationChange }: FileUploadProps) => {
const { eventState } = useFunnelState();

const FileUpload = ({
value = '',
onChange,
eventState,
setEventState,
useDefaultImage,
onValidationChange,
}: FileUploadProps) => {
const { previewUrl, fileInputRef, handleFileChange, handleDrop, setIsDragging, isDragging } = useImageUpload({
value: eventState.bannerImageUrl,
value: value ?? eventState?.bannerImageUrl ?? '',
onSuccess: url => {
onChange?.(url);
setEventState?.(prev => ({ ...prev, bannerImageUrl: url }));
Expand Down
23 changes: 14 additions & 9 deletions src/features/event/ui/LinkInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useFunnelState } from '../model/FunnelContext';
import { FunnelState } from '../model/FunnelContext';
import AddButton from '../../../../public/assets/event-manage/creation/AddBtn.svg';
import CloseButton from '../../../../public/assets/event-manage/creation/CloseBtn.svg';
import Link from '../../../../public/assets/event-manage/creation/Link.svg';
Expand All @@ -9,9 +9,15 @@ export interface Link {
url: string;
}

const LinkInput = () => {
const { eventState, setEventState } = useFunnelState();
const links = eventState.referenceLinks;
interface LinkInputProps {
value?: Link[];
onChange?: (value: Link[]) => void;
eventState?: FunnelState['eventState'];
setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>;
}

const LinkInput = ({ value, onChange, eventState, setEventState }: LinkInputProps) => {
const links = value ?? eventState?.referenceLinks ?? [];

const [activeInput, setActiveInput] = useState<{ field: 'title' | 'url' | null }>({
field: null,
Expand All @@ -21,20 +27,19 @@ const LinkInput = () => {
});

const updateAll = (newLinks: Link[]) => {
onChange?.(newLinks);
setEventState?.(prev => ({ ...prev, referenceLinks: newLinks }));
};

const addNewLink = () => {
updateAll([...(links || []), { title: '', url: '' }]);
};
const addNewLink = () => updateAll([...links, { title: '', url: '' }]);

const removeLink = (index: number) => {
const newLinks = links.filter((_, i) => i !== index);
updateAll(newLinks);
if (newLinks) updateAll(newLinks);
};

const updateLink = (index: number, field: keyof Link, value: string) => {
const newLinks = [...links];
const newLinks = [...(links ?? [])];
newLinks[index] = { ...newLinks[index], [field]: value };
updateAll(newLinks);
};
Expand Down
40 changes: 27 additions & 13 deletions src/features/event/ui/TextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import { uploadFile } from '../hooks/usePresignedUrlHook';
import { useFunnelState } from '../model/FunnelContext';
import { FunnelState } from '../model/FunnelContext';

interface TextEditorProps {
eventState?: FunnelState['eventState'];
setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>;
value?: string;
onChange?: (value: string) => void;
onValidationChange?: (isValid: boolean) => void;
}
Comment on lines 7 to 13
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

Props 인터페이스 설계를 개선해야 합니다.

현재 설계는 제어형(value/onChange)과 비제어형(eventState/setEventState) 패턴을 동시에 지원하려고 하는데, 이는 사용자에게 혼란을 줄 수 있습니다. 어떤 props가 우선순위를 갖는지 명확하지 않습니다.

interface TextEditorProps {
-  eventState?: FunnelState['eventState'];
-  setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>;
-  value?: string;
-  onChange?: (value: string) => void;
+  // 제어형 패턴 또는 비제어형 패턴 중 하나를 선택
+  value: string;
+  onChange: (value: string) => void;
  onValidationChange?: (isValid: boolean) => void;
}
📝 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
interface TextEditorProps {
eventState?: FunnelState['eventState'];
setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>;
value?: string;
onChange?: (value: string) => void;
onValidationChange?: (isValid: boolean) => void;
}
interface TextEditorProps {
// 제어형 패턴 또는 비제어형 패턴 중 하나를 선택
value: string;
onChange: (value: string) => void;
onValidationChange?: (isValid: boolean) => void;
}
🤖 Prompt for AI Agents
In src/features/event/ui/TextEditor.tsx lines 7 to 13, the TextEditorProps
interface mixes controlled (value/onChange) and uncontrolled
(eventState/setEventState) patterns, causing ambiguity. Refactor the props to
clearly separate or choose one pattern, ensuring only one control method is used
at a time. Define which props take precedence or create distinct interfaces for
controlled and uncontrolled usage to avoid confusion.


Expand All @@ -31,11 +35,19 @@ const formats = [
'h1',
];

const TextEditor = ({ onValidationChange }: TextEditorProps) => {
const { eventState, setEventState } = useFunnelState();
const TextEditor = ({ eventState, setEventState, value = '', onChange, onValidationChange }: TextEditorProps) => {
const quillRef = useRef<ReactQuill | null>(null);

const [editorContent, setEditorContent] = useState('');
const [isOverLimit, setIsOverLimit] = useState(false);

useEffect(() => {
const initial = value ?? eventState?.description ?? '';
if (!editorContent && initial) {
setEditorContent(initial);
}
}, [value, eventState?.description, editorContent]);
Comment on lines +44 to +49
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

초기화 로직의 우선순위가 불분명합니다.

valueeventState?.description 중 어떤 것이 우선순위를 갖는지 명확하지 않습니다. 또한 editorContent가 비어있을 때만 초기화하는 조건이 예상과 다르게 동작할 수 있습니다.

useEffect(() => {
-  const initial = value ?? eventState?.description ?? '';
-  if (!editorContent && initial) {
-    setEditorContent(initial);
-  }
-}, [value, eventState?.description, editorContent]);
+  // value prop이 제공되면 항상 해당 값을 사용
+  if (value !== undefined) {
+    setEditorContent(value);
+  }
+}, [value]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/features/event/ui/TextEditor.tsx around lines 44 to 49, clarify the
priority between `value` and `eventState?.description` by explicitly documenting
or restructuring the initialization logic. Also, revise the condition that
checks if `editorContent` is empty before setting it to ensure it behaves as
intended, possibly by using a more precise check or resetting logic to avoid
unexpected behavior.


const imageHandler = async () => {
if (!quillRef.current) return;

Expand Down Expand Up @@ -74,25 +86,27 @@ const TextEditor = ({ onValidationChange }: TextEditorProps) => {
return textLength + imageCount * IMAGE_WEIGHT;
};

const handleChange = (value: string) => {
const totalLength = getTotalContentLength(value);
const handleChange = (val: string) => {
const totalLength = getTotalContentLength(val);

if (totalLength <= MAX_LENGTH) {
setEventState?.(prev => ({ ...prev, description: value }));
onValidationChange?.(getPlainText(value).length > 0);
setEditorContent(val);
onChange?.(val);
setEventState?.(prev => ({ ...prev, description: val }));
onValidationChange?.(getPlainText(val).length > 0);
setIsOverLimit(false);
} else {
const editorInstance = quillRef.current?.getEditor();
if (editorInstance) {
editorInstance.setContents(editorInstance.clipboard.convert(eventState.description));
editorInstance.setContents(editorInstance.clipboard.convert(eventState?.description));
}
setIsOverLimit(true);
}
};
Comment on lines +89 to 105
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

handleChange 함수의 복잡성을 줄여야 합니다.

현재 함수는 로컬 상태와 외부 상태를 동시에 업데이트하여 상태 불일치 가능성이 있습니다. 또한 오류 시 fallback 로직에서 eventState?.description을 사용하는데, value prop을 통해 제어되는 경우 적절하지 않습니다.

const handleChange = (val: string) => {
  const totalLength = getTotalContentLength(val);

  if (totalLength <= MAX_LENGTH) {
    setEditorContent(val);
    onChange?.(val);
-    setEventState?.(prev => ({ ...prev, description: val }));
    onValidationChange?.(getPlainText(val).length > 0);
    setIsOverLimit(false);
  } else {
    const editorInstance = quillRef.current?.getEditor();
    if (editorInstance) {
-      editorInstance.setContents(editorInstance.clipboard.convert(eventState?.description));
+      // 현재 유효한 내용으로 되돌리기
+      editorInstance.setContents(editorInstance.clipboard.convert(editorContent));
    }
    setIsOverLimit(true);
  }
};
📝 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
const handleChange = (val: string) => {
const totalLength = getTotalContentLength(val);
if (totalLength <= MAX_LENGTH) {
setEventState?.(prev => ({ ...prev, description: value }));
onValidationChange?.(getPlainText(value).length > 0);
setEditorContent(val);
onChange?.(val);
setEventState?.(prev => ({ ...prev, description: val }));
onValidationChange?.(getPlainText(val).length > 0);
setIsOverLimit(false);
} else {
const editorInstance = quillRef.current?.getEditor();
if (editorInstance) {
editorInstance.setContents(editorInstance.clipboard.convert(eventState.description));
editorInstance.setContents(editorInstance.clipboard.convert(eventState?.description));
}
setIsOverLimit(true);
}
};
const handleChange = (val: string) => {
const totalLength = getTotalContentLength(val);
if (totalLength <= MAX_LENGTH) {
setEditorContent(val);
onChange?.(val);
onValidationChange?.(getPlainText(val).length > 0);
setIsOverLimit(false);
} else {
const editorInstance = quillRef.current?.getEditor();
if (editorInstance) {
// 현재 유효한 내용으로 되돌리기
editorInstance.setContents(
editorInstance.clipboard.convert(editorContent)
);
}
setIsOverLimit(true);
}
};
🤖 Prompt for AI Agents
In src/features/event/ui/TextEditor.tsx around lines 89 to 105, the handleChange
function updates both local and external states, risking state inconsistencies,
and uses eventState?.description in fallback which is inappropriate if value
prop controls the content. Refactor handleChange to separate concerns by
updating only one source of truth for the editor content, preferably relying on
the value prop or a single state source, and adjust the fallback logic to use
the current valid content from props or state instead of eventState?.description
to ensure consistency and reduce complexity.


useEffect(() => {
onValidationChange?.(getPlainText(eventState.description).length > 0);
}, [eventState.description, onValidationChange]);
onValidationChange?.(getPlainText(editorContent).length > 0);
}, [editorContent, onValidationChange]);

const modules = useMemo(
() => ({
Expand All @@ -115,15 +129,15 @@ const TextEditor = ({ onValidationChange }: TextEditorProps) => {
[]
);

const totalLength = getTotalContentLength(eventState.description);
const imageCount = getImageCount(eventState.description);
const totalLength = getTotalContentLength(editorContent);
const imageCount = getImageCount(editorContent);

return (
<div className="flex flex-col justify-start gap-2 mb-4">
<h1 className="font-bold text-black text-lg">이벤트에 대한 상세 설명</h1>
<ReactQuill
theme="snow"
value={eventState.description}
value={editorContent}
ref={quillRef}
modules={modules}
formats={formats}
Expand Down
6 changes: 3 additions & 3 deletions src/pages/dashboard/ui/EventDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ const EventDetailPage = () => {
<DashboardLayout centerContent="대시보드">
<div className="flex flex-col gap-5 mt-8 px-7">
<h1 className="text-center text-xl font-bold mb-5">이벤트 상세 정보</h1>
<FileUpload onChange={setBannerImageUrl} useDefaultImage={false} />
<TextEditor />
<LinkInput />
<FileUpload value={bannerImageUrl} onChange={setBannerImageUrl} useDefaultImage={false} />
<TextEditor value={description} />
<LinkInput value={referenceLinks} onChange={setReferenceLinks} />
</div>
<div className="w-full p-7">
<Button label="저장하기" onClick={handleSave} className="w-full h-12 rounded-full" />
Expand Down
2 changes: 1 addition & 1 deletion src/pages/dashboard/ui/EventInfoPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const EventInfoPage = () => {
category: data.result.category || 'DEVELOPMENT_STUDY',
hashtags: data.result.hashtags || [],
organizerEmail: email || data.result.organizerEmail || '',
organizerPhoneNumber: phone.replace(/-/g, '') || data.result.organizerPhoneNumber || '',
organizerPhoneNumber: phone || data.result.organizerPhoneNumber || '',
};

mutate(requestData, {
Expand Down
13 changes: 9 additions & 4 deletions src/pages/event/ui/create-event/EventInfoPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface EventInfoPageProps {
}

const EventInfoPage = ({ onValidationChange }: EventInfoPageProps) => {
const { setEventState } = useFunnelState();
const { eventState, setEventState } = useFunnelState();
const [isFileValid, setIsFileValid] = useState(false);
const [isTextValid, setIsTextValid] = useState(false);

Expand All @@ -27,9 +27,14 @@ const EventInfoPage = ({ onValidationChange }: EventInfoPageProps) => {

return (
<div className="w-full px-5 space-y-8">
<FileUpload setEventState={setEventState} useDefaultImage={false} onValidationChange={handleFileValidation} />
<TextEditor onValidationChange={handleTextValidation} />
<LinkInput />
<FileUpload
eventState={eventState}
setEventState={setEventState}
useDefaultImage={false}
onValidationChange={handleFileValidation}
/>
<TextEditor eventState={eventState} setEventState={setEventState} onValidationChange={handleTextValidation} />
<LinkInput eventState={eventState} setEventState={setEventState} />
</div>
);
};
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