Skip to content

feat: 이벤트 배너 이미지 형식을 WebP로 전환하여 이미지 성능 개선#198

Merged
Yejiin21 merged 6 commits intodevelopfrom
feat/#196/banner-image-webP
Jun 26, 2025
Merged

feat: 이벤트 배너 이미지 형식을 WebP로 전환하여 이미지 성능 개선#198
Yejiin21 merged 6 commits intodevelopfrom
feat/#196/banner-image-webP

Conversation

@Yejiin21
Copy link
Copy Markdown
Contributor

@Yejiin21 Yejiin21 commented Jun 26, 2025

문제 정의

사용자가 접근 시, 티켓 목록 이미지가 늦게 로딩되며 첫 화면이 완전히 보이기까지 지연이 발생함. 이로 인해 사용자 체감 성능 저하, Lighthouse 성능 점수 하락(LCP 지연)이 확인됨.

문제 원인 분석

Lighthouse 측정 결과:

  • Performance: 56점
  • 주요 성능 지표
    • FCP (First Contentful Paint): 4.9초
    • LCP (Largest Contentful Paint): 6.4초
    • Speed Index: 7.9초

-> Lighthouse의 진단 결과를 보면 가장 큰 문제는 “이미지”에 있었다.

해결 과정

유저가 이벤트 배너 이미지를 업로드하는 과정에서 이미지 형식을 webP로 바꿔서 업로드되도록 하는convertImageToWebP을 구현하여 이미지 성능 개선

결과

  • Performance 56점 → 75점 증가 (19점 증가)
  • LCP 6110ms → 5070ms 감소 (17% 감소)
  • 이미지 용량 787kB→ 84.4kB 감소 (89% 감소)
  • 네트워크 탭의 타임(이미지를 불러오는 시간) 662ms → 112ms 감소 (83% 감소)

Summary by CodeRabbit

  • 신규 기능

    • 이미지 업로드 시 자동으로 WebP 형식으로 변환되어 업로드됩니다.
    • JPG 이미지를 WebP로 일괄 변환 및 업로드하는 마이그레이션 기능이 추가되었습니다.
    • 이벤트 관련 컴포넌트(FileUpload, TextEditor, LinkInput)가 외부 상태 제어를 지원하도록 개선되었습니다.
  • 버그 수정

    • 업로드 이미지의 Content-Type이 항상 'image/webp'로 지정됩니다.
    • 이벤트 정보 저장 시 전화번호의 하이픈 제거가 중단되어 원본 형식이 유지됩니다.
  • 환경 설정

    • 로컬 개발 서버의 HTTPS 설정이 비활성화되어 HTTP만 지원됩니다.

@Yejiin21 Yejiin21 self-assigned this Jun 26, 2025
@Yejiin21 Yejiin21 added the 🔧 Feature 기능 구현 label Jun 26, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jun 26, 2025

"""

Walkthrough

이미지 업로드 및 변환 로직이 개선되어, 모든 업로드 이미지가 WebP 포맷으로 변환된 후 업로드되도록 변경되었습니다. 이를 위해 WebP 변환 유틸리티가 추가되었고, JPG 이미지를 WebP로 일괄 변환하는 마이그레이션 스크립트도 도입되었습니다. 개발 서버의 HTTPS 설정은 비활성화되었으며, 주요 UI 컴포넌트들이 외부 상태 제어를 지원하도록 리팩토링되었습니다.

Changes

파일/경로 변경 요약
src/features/event/hooks/usePresignedUrlHook.ts, src/shared/hooks/useImageUpload.ts 이미지 업로드 전 WebP 포맷으로 변환하는 단계 추가 및 업로드 로직 수정
src/shared/lib/convertImageToWebP.ts 이미지 파일을 WebP로 변환하는 유틸리티 함수 신규 추가
src/shared/lib/migrateJpgToWebp.ts JPG 이미지를 WebP로 일괄 변환 및 업로드하는 마이그레이션 스크립트 추가
vite.config.ts fs 모듈 및 HTTPS 서버 설정 주석 처리, 로컬 개발 서버 HTTP만 활성화
src/features/event/ui/FileUpload.tsx, src/features/event/ui/LinkInput.tsx, src/features/event/ui/TextEditor.tsx 주요 UI 컴포넌트들이 내부 컨텍스트 의존성 제거 및 외부 상태 제어 지원하도록 리팩토링
src/pages/dashboard/ui/EventDetailPage.tsx FileUpload, TextEditor, LinkInput 컴포넌트에 value props 전달하여 제어 컴포넌트로 변경
src/pages/event/ui/create-event/EventInfoPage.tsx EventInfoPage에서 eventState와 setEventState를 자식 컴포넌트에 전달하도록 수정
src/pages/dashboard/ui/EventInfoPage.tsx organizerPhoneNumber 저장 시 전화번호의 하이픈 제거 로직 삭제

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant convertImageToWebP
    participant S3

    User->>Frontend: 이미지 파일 선택
    Frontend->>convertImageToWebP: 파일 WebP로 변환 요청
    convertImageToWebP-->>Frontend: WebP 파일 반환
    Frontend->>S3: WebP 파일 presigned URL 요청 및 업로드
    S3-->>Frontend: 업로드 완료 응답
    Frontend-->>User: 업로드 성공 알림
Loading

Possibly related PRs

Poem

🐰
이미지는 WebP로 변신해,
토끼는 깡충 뛰며 노래해!
JPG는 이제 안녕,
변환 스크립트가 출동 중.
개발 서버는 HTTP로 쓩—
새로운 변화, 토끼도 흥!

"""

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🔭 Outside diff range comments (1)
src/features/event/hooks/usePresignedUrlHook.ts (1)

38-50: 중복 WebP 변환 문제

uploadFile 함수에서 WebP 변환이 수행되는데, useImageUpload.ts에서도 이미 WebP 변환을 수행한 후 이 함수를 호출합니다. 이는 이미 WebP인 파일을 다시 변환하려고 시도하여 비효율적이거나 오류를 발생시킬 수 있습니다.

다음 중 하나의 방법으로 해결하세요:

방법 1: uploadFile에서 변환 로직 제거

export const uploadFile = async (file: File) => {
-  const webFile = await convertImageToWebP(file);
-  const fileName = webFile.name;
+  const fileName = file.name;

  const presignedUrlResponse = await getPresignedUrl({ fileName });

  if (!presignedUrlResponse) {
    throw new Error('Failed to get presigned url');
  }

  const url = presignedUrlResponse;
  console.log('Presigned URL:', url);

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

  return url.split('?')[0];
};

방법 2: 파일이 이미 WebP인지 확인

export const uploadFile = async (file: File) => {
-  const webFile = await convertImageToWebP(file);
+  const webFile = file.type === 'image/webp' ? file : await convertImageToWebP(file);
  const fileName = webFile.name;
  // ... 나머지 코드
};
🧹 Nitpick comments (3)
src/shared/hooks/useImageUpload.ts (1)

33-36: 파일 검증 로직 개선 필요

현재 검증 로직이 WebP 변환 전 원본 파일에 대해서만 수행됩니다. WebP 변환 후 파일 크기가 줄어들 수 있으므로, 검증 순서나 메시지를 조정하는 것을 고려해보세요.

  const validateFile = (file: File) => {
    if (file.size > 1000 * 1024) {
-     alert('파일 크기는 1MB를 초과할 수 없습니다.');
+     alert('파일 크기는 1MB를 초과할 수 없습니다. (WebP 변환 후 크기가 줄어듭니다)');
      return false;
    }
    if (!['image/jpg', 'image/jpeg', 'image/png'].includes(file.type)) {
-     alert('jpg, jpeg, png 파일만 업로드 가능합니다.');
+     alert('jpg, jpeg, png 파일만 업로드 가능합니다. (WebP로 자동 변환됩니다)');
      return false;
    }
    return true;
  };
src/shared/lib/migrateJpgToWebp.ts (1)

4-6: 마이그레이션 대상 URL 배열이 비어있음

현재 oldJpgUrls 배열이 비어있습니다. 실제 마이그레이션 시에는 변환할 이미지 URL들을 추가해야 합니다.

실제 마이그레이션을 위한 URL 목록이 필요하시면 도움을 드릴 수 있습니다.

src/shared/lib/convertImageToWebP.ts (1)

1-36: WebP 변환 구현 품질 우수

전반적으로 잘 구현된 WebP 변환 유틸리티입니다. 에러 핸들링이 적절하고 Promise 기반 비동기 처리가 올바릅니다.

개선 제안사항:

  1. WebP 품질 옵션 추가: 현재 기본 품질로 변환되는데, 품질 조절 옵션을 추가하면 더 유연합니다.
  2. 브라우저 호환성 확인: WebP 지원 여부를 확인하는 로직을 추가하세요.
  3. 이미지 크기 제한: 매우 큰 이미지의 경우 메모리 사용량이 클 수 있습니다.
-export const convertImageToWebP = (file: File): Promise<File> => {
+export const convertImageToWebP = (file: File, quality: number = 0.8): Promise<File> => {
  return new Promise((resolve, reject) => {
+    // WebP 지원 확인
+    const canvas = document.createElement('canvas');
+    const ctx = canvas.getContext('2d');
+    if (!ctx || !canvas.toBlob) {
+      return reject(new Error('WebP 변환이 지원되지 않는 브라우저입니다.'));
+    }
+
    const img = new Image();
    const reader = new FileReader();

    // ... 기존 로직 ...

    img.onload = () => {
+      // 이미지 크기 제한 (예: 4K)
+      const MAX_DIMENSION = 4096;
+      let { width, height } = img;
+      
+      if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
+        const ratio = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height);
+        width *= ratio;
+        height *= ratio;
+      }
+
-      const canvas = document.createElement('canvas');
-      canvas.width = img.width;
-      canvas.height = img.height;
+      canvas.width = width;
+      canvas.height = height;

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

-      ctx.drawImage(img, 0, 0);
+      ctx.drawImage(img, 0, 0, width, height);

-      canvas.toBlob(blob => {
+      canvas.toBlob(blob => {
        // ... 나머지 로직 동일 ...
-      }, 'image/webp');
+      }, 'image/webp', quality);
    };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ba4f5d and 7dbae90.

📒 Files selected for processing (5)
  • src/features/event/hooks/usePresignedUrlHook.ts (4 hunks)
  • src/shared/hooks/useImageUpload.ts (2 hunks)
  • src/shared/lib/convertImageToWebP.ts (1 hunks)
  • src/shared/lib/migrateJpgToWebp.ts (1 hunks)
  • vite.config.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/shared/hooks/useImageUpload.ts (2)
src/shared/lib/convertImageToWebP.ts (1)
  • convertImageToWebP (1-36)
src/features/event/hooks/usePresignedUrlHook.ts (1)
  • uploadFile (37-54)
src/features/event/hooks/usePresignedUrlHook.ts (1)
src/shared/lib/convertImageToWebP.ts (1)
  • convertImageToWebP (1-36)
src/shared/lib/migrateJpgToWebp.ts (2)
src/shared/lib/convertImageToWebP.ts (1)
  • convertImageToWebP (1-36)
src/features/event/hooks/usePresignedUrlHook.ts (1)
  • uploadFile (37-54)
🔇 Additional comments (6)
vite.config.ts (1)

6-6: 로컬 개발용 HTTPS 비활성화 확인

HTTPS 설정이 주석 처리되어 있습니다. 로컬 개발 목적이라면 문제없지만, 프로덕션 배포 시에는 다시 활성화해야 합니다.

Also applies to: 23-27

src/shared/hooks/useImageUpload.ts (2)

3-3: WebP 변환 유틸리티 import 추가 확인

WebP 변환 기능이 올바르게 import되었습니다.


45-46: WebP 변환 로직 구현 확인

WebP 변환이 올바르게 구현되었습니다. 에러 핸들링도 적절합니다.

src/features/event/hooks/usePresignedUrlHook.ts (2)

5-5: WebP 변환 유틸리티 import 추가 확인

WebP 변환 기능이 올바르게 import되었습니다.


27-27: Content-Type 헤더 올바르게 설정

WebP 파일 업로드를 위해 Content-Type이 올바르게 설정되었습니다.

src/shared/lib/migrateJpgToWebp.ts (1)

28-31: 파일명 추출 로직 확인

파일명 추출 및 확장자 변환 로직이 올바르게 구현되었습니다. URL 파라미터 제거와 확장자 변환이 적절합니다.

Comment on lines +8 to +26
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('✅ 전체 마이그레이션 완료');
};
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.

@Yejiin21 Yejiin21 force-pushed the feat/#196/banner-image-webP branch from 7dbae90 to 286e15d Compare June 26, 2025 03:58
@github-actions
Copy link
Copy Markdown

Copy link
Copy Markdown
Member

@m3k0813 m3k0813 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~

@github-actions
Copy link
Copy Markdown

@Yejiin21 Yejiin21 merged commit dfcb2d1 into develop Jun 26, 2025
1 of 2 checks passed
@Yejiin21 Yejiin21 deleted the feat/#196/banner-image-webP branch June 26, 2025 06:14
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/features/event/ui/FileUpload.tsx (1)

24-24: 이미지 URL 초기화 로직 개선 제안

nullish coalescing 연산자를 사용한 이미지 URL 초기화 로직이 올바르게 구현되었습니다만, 더 명확한 우선순위 처리를 위해 개선할 수 있습니다.

-value: value ?? eventState?.bannerImageUrl ?? '',
+value: value || eventState?.bannerImageUrl || '',

또는 더 명시적으로:

-value: value ?? eventState?.bannerImageUrl ?? '',
+value: value !== undefined ? value : (eventState?.bannerImageUrl || ''),
src/features/event/ui/LinkInput.tsx (2)

37-39: 불필요한 조건 검사 제거 가능

newLinks 변수는 항상 배열이므로 조건 검사가 불필요합니다.

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

41-45: 링크 업데이트 로직 개선 제안

링크 업데이트 로직이 올바르게 구현되었습니다만, 안전성을 위해 개선할 수 있습니다.

-const updateLink = (index: number, field: keyof Link, value: string) => {
-  const newLinks = [...(links ?? [])];
-  newLinks[index] = { ...newLinks[index], [field]: value };
-  updateAll(newLinks);
-};
+const updateLink = (index: number, field: keyof Link, value: string) => {
+  if (index < 0 || index >= links.length) return;
+  const newLinks = [...links];
+  newLinks[index] = { ...newLinks[index], [field]: value };
+  updateAll(newLinks);
+};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 286e15d and 8c72c59.

📒 Files selected for processing (6)
  • src/features/event/ui/FileUpload.tsx (1 hunks)
  • src/features/event/ui/LinkInput.tsx (3 hunks)
  • src/features/event/ui/TextEditor.tsx (4 hunks)
  • src/pages/dashboard/ui/EventDetailPage.tsx (1 hunks)
  • src/pages/dashboard/ui/EventInfoPage.tsx (1 hunks)
  • src/pages/event/ui/create-event/EventInfoPage.tsx (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/pages/event/ui/create-event/EventInfoPage.tsx (1)
src/features/event/model/FunnelContext.tsx (1)
  • useFunnelState (53-59)
src/features/event/ui/FileUpload.tsx (1)
src/features/event/model/FunnelContext.tsx (1)
  • FunnelState (5-11)
src/features/event/ui/LinkInput.tsx (1)
src/features/event/model/FunnelContext.tsx (1)
  • FunnelState (5-11)
src/features/event/ui/TextEditor.tsx (1)
src/features/event/model/FunnelContext.tsx (1)
  • FunnelState (5-11)
🔇 Additional comments (11)
src/pages/dashboard/ui/EventInfoPage.tsx (1)

58-58: ripgrep이 --type tsx를 인식하지 못해 검색이 누락되었습니다. glob 패턴으로 확장자를 지정해 다시 확인해 주세요:

#!/bin/bash
echo "=== 하이픈 제거 로직 검색 (.replace(/-/g, '')) ==="
rg "\\.replace\\(/-\\/g, ''\\)" -g '*.ts' -g '*.tsx'

echo "=== organizerPhoneNumber 사용 위치 검색 ==="
rg "organizerPhoneNumber" -g '*.ts' -g '*.tsx'
src/pages/dashboard/ui/EventDetailPage.tsx (1)

69-71: 제어형 컴포넌트 구현이 올바르게 적용되었습니다

FileUpload, TextEditor, LinkInput 컴포넌트에 value prop이 올바르게 전달되어 외부 상태 제어가 가능하도록 구현되었습니다.

다만, 컴포넌트 인터페이스를 고려할 때 일부 prop이 누락될 수 있습니다:

-<FileUpload value={bannerImageUrl} onChange={setBannerImageUrl} useDefaultImage={false} />
-<TextEditor value={description} />
-<LinkInput value={referenceLinks} onChange={setReferenceLinks} />
+<FileUpload value={bannerImageUrl} onChange={setBannerImageUrl} useDefaultImage={false} />
+<TextEditor value={description} onChange={setDescription} />
+<LinkInput value={referenceLinks} onChange={setReferenceLinks} />
src/pages/event/ui/create-event/EventInfoPage.tsx (2)

12-12: 상태 추출 및 제어형 컴포넌트 패턴 올바르게 구현

useFunnelState에서 eventState와 setEventState를 추출하여 하위 컴포넌트에 전달하는 패턴이 올바르게 구현되었습니다.


30-37: 하위 컴포넌트에 상태 전달 구현 완료

FileUpload, TextEditor, LinkInput 컴포넌트에 eventState와 setEventState가 올바르게 전달되어 제어형 컴포넌트로 동작할 수 있도록 구현되었습니다.

src/features/event/ui/FileUpload.tsx (1)

7-13: 제어형 컴포넌트 인터페이스 올바르게 정의

value와 eventState prop을 추가하여 외부 상태 제어가 가능하도록 인터페이스가 올바르게 정의되었습니다.

src/features/event/ui/LinkInput.tsx (3)

13-17: 제어형 컴포넌트 인터페이스 올바르게 정의

value, onChange, eventState, setEventState prop을 추가하여 외부 상태 제어가 가능하도록 인터페이스가 올바르게 정의되었습니다.


20-20: 링크 배열 초기화 로직 올바르게 구현

value prop을 우선적으로 사용하고 eventState를 fallback으로 사용하는 로직이 올바르게 구현되었습니다.


29-32: 상태 업데이트 패턴 올바르게 구현

onChange 콜백을 먼저 호출한 후 setEventState를 호출하는 패턴이 올바르게 구현되어 외부 상태와 내부 상태가 동기화됩니다.

src/features/event/ui/TextEditor.tsx (3)

5-5: 임포트 변경이 적절합니다.

내부 context hook 사용에서 타입만 직접 임포트하는 방식으로 변경한 것이 컴포넌트의 외부 제어 패턴에 적합합니다.


107-109: 검증 로직이 개선되었습니다.

editorContent를 사용하여 검증하는 것이 일관성 있는 접근입니다.


132-133: 렌더링 로직이 올바르게 개선되었습니다.

로컬 상태 editorContent를 사용하여 렌더링하는 것이 일관된 접근입니다.

Also applies to: 140-140

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

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

Comment on lines +89 to 105
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);
}
};
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔧 Feature 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 이벤트 배너 이미지 형식을 WebP로 전환하여 이미지 성능 개선

2 participants