Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Change Summary |
|---|---|
Tooling & Buildfrontend/package.json, frontend/vite.config.ts |
Webpack → Vite/Vike 전환: scripts 변경(vike preview, vike build 등), @vitejs/plugin-react, hono, vike, vike-server 등 의존 추가 및 react-router-dom 버전 업데이트; Vite 설정 추가(플러그인, alias, SSR noExternal). |
Docsfrontend/docs/vike-migration-strategy.md |
Vike SSR 마이그레이션 전략 문서 신규 추가(설정·구조·예시 코드 포함). |
Routing Entrypointfrontend/src/pages/+Page.jsx, frontend/src/pages/+route.js |
Vike용 catch-all 페이지 추가: +Page.jsx(App 렌더) 및 +route.js('/*' 기본 export). |
App changesfrontend/src/App.tsx |
BrowserRouter 제거 후 최상위에서 Routes 사용; QueryClient를 lazy하게 state로 초기화하도록 변경. |
SSR Rendererfrontend/src/pages/renderer/+config.h.js, .../+onRenderClient.jsx, .../+onRenderHtml.jsx |
Vike 렌더러 추가: passToClient 설정, 클라이언트 하이드레이션 엔트리(SDK 초기화, hydrateRoot + BrowserRouter), 서버 HTML 렌더러(StaticRouter + renderToString, escapeInject). |
Server Entryfrontend/src/pages/server/index.ts |
Hono 서버 추가 및 vike-server 미들웨어 적용, 기본 포트 3000으로 serve. |
Env Vars Migrationfrontend/src/constants/api.ts, frontend/src/utils/initSDK.ts |
CRA process.env.* → Vite import.meta.env.VITE_*로 환경변수 소스 변경 (VITE_API_BASE_URL, VITE_MIXPANEL_TOKEN). |
Responsive & UI tweaksfrontend/src/hooks/useIsMobile.ts, frontend/src/pages/MainPage/components/Banner/Banner.tsx, frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx |
isMobile 초기 상태를 false로 변경하고 useEffect에서 초기 검사 실행; Banner에 helper/debounce 적용; ClubCard는 JSX 포맷팅 변경(무해한 스타일 변경). |
Sequence Diagram(s)
sequenceDiagram
autonumber
actor U as 사용자
participant B as 브라우저
participant H as Hono 서버
participant V as Vike 미들웨어
participant R as SSR Renderer\n(+onRenderHtml)
participant C as CSR Hydrator\n(+onRenderClient)
U->>B: URL 요청
B->>H: HTTP GET /
H->>V: vike-server 미들웨어
V->>R: onRenderHtml(pageContext)
R->>R: StaticRouter로 SSR 마크업 생성
R-->>V: 완성된 HTML
V-->>H: HTML 반환
H-->>B: 응답 전송
Note over B: 클라이언트 스크립트 로드 후
B->>C: onRenderClient(pageContext) 실행
C->>C: SDK 초기화 (Mixpanel/Channel/Sentry/Kakao)
C->>C: hydrateRoot + BrowserRouter로 하이드레이트
C-->>U: 상호작용 가능한 앱
sequenceDiagram
autonumber
participant App as App.tsx
participant RR as React Router
participant VEnv as import.meta.env
participant API as API_BASE_URL
participant Mix as initializeMixpanel
App->>RR: Routes 렌더 (CSR 진입에서 BrowserRouter로 래핑)
API->>VEnv: VITE_API_BASE_URL 조회
Mix->>VEnv: VITE_MIXPANEL_TOKEN 조회
Note over App: useIsMobile 등은 마운트 시 초기화됨
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
- [fix] 모바일 환경에서 배너 슬라이드 초기 렌더링 문제 해결 #381 — 동일 Banner 컴포넌트의 초기화/리사이즈 동작 변경과 관련된 PR로, isMobile 초기화/리스너 동작의 동기화 문제와 코드 충돌 가능성이 높음.
Suggested labels
✨ Feature, 💻 FE, ⚙ Setting
Suggested reviewers
- oesnuj
- seongwon030
Pre-merge checks and finishing touches
❌ Failed checks (3 warnings)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Title Check | 제목은 동아리별 동적 OG 이미지 생성 기능을 설명하고 있지만 실제 변경사항은 Vike 기반 SSR 마이그레이션과 빌드 스크립트 수정에 집중되어 있어 OG 이미지 생성과 직접적인 연관이 없습니다. | 제목을 실제 변경된 Vite/SSR 마이그레이션 작업을 반영하도록 수정하거나, OG 이미지 생성 기능 구현을 PR에 추가하여 제목과 내용이 일치하도록 맞추세요. | |
| Linked Issues Check | 링크된 MOA-260 이슈는 동적 OG 이미지 서버 구축과 메타 태그 삽입을 요구하는데 PR 변경사항에는 해당 기능 구현이 전혀 포함되어 있지 않고 오히려 SSR 마이그레이션 작업만 수행되어 이슈 요구사항을 충족하지 못합니다. | 다음JS 이미지 서버 설정과 동적 메타 태그 삽입 코드를 추가하여 이슈의 체크리스트를 완전히 구현하거나, 이슈에 맞는 별도 코드를 작성해 요구사항을 충족하세요. | |
| Out of Scope Changes Check | PR에는 OG 이미지 생성과 무관한 Vike SSR 도입, Vite 설정, Hono 서버 구성, 페이지 렌더러 추가 등 광범위한 마이그레이션 코드가 포함되어 있어 링크된 이슈 범위를 벗어난 과도한 변경이 다수 포함되어 있습니다. | OG 이미지 기능 구현과 관련된 변경만을 남기고 나머지 SSR 마이그레이션은 별도의 PR로 분리하여 관리하거나, PR 범위를 재조정해 이슈 목적에 집중하도록 수정하세요. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Docstring Coverage | ✅ Passed | No functions found in the changes. Docstring coverage check skipped. |
✨ Finishing touches
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
feature/#757-support-dynamic-og-image-MOA-260
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.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
frontend/config/webpack.common.ts (1)
3-3: 구성 참조 방향 OK이나, 유틸 위치 명확화 고려빌드 타임 전용 유틸을 src 아래 두면 번들 의존성으로 오인될 수 있습니다. 차후 config/utils 또는 scripts/prerender 아래로 이동을 고려해 주세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (4)
frontend/config/webpack.common.ts(2 hunks)frontend/package.json(1 hunks)frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx(1 hunks)frontend/src/utils/dynamicPath.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.
Files:
frontend/src/utils/dynamicPath.tsfrontend/src/pages/ClubDetailPage/ClubDetailPage.tsxfrontend/config/webpack.common.ts
frontend/**/*.tsx
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.
Files:
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
🧠 Learnings (2)
📓 Common learnings
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
Applied to files:
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
🔇 Additional comments (3)
frontend/package.json (1)
48-49: prerenderer 의존성 추가는 타당합니다. CI 런타임 요구사항 확인만 부탁드립니다.Puppeteer 사용으로 인해 CI에 필요한 시스템 라이브러리/폰트가 누락되면 실패할 수 있습니다. 또한 프리렌더를 토글할 npm 스크립트를 추가하면 운영이 편합니다.
프리렌더 토글 스크립트 예시(추가 제안):
"scripts": { "dev": "tsx --no-cache ./node_modules/webpack-cli/bin/cli.js serve --config config/webpack.dev.ts", "build:dev": "tsx ./node_modules/webpack-cli/bin/cli.js --config config/webpack.dev.ts", "build:prod": "tsx ./node_modules/webpack-cli/bin/cli.js --config config/webpack.prod.ts", + "build:prerender": "PRERENDER=true NODE_ENV=production tsx ./node_modules/webpack-cli/bin/cli.js --config config/webpack.prod.ts",검증 스크립트(의존성/피어 확인):
또한, 문서 확인이 필요하면 아래 질의를 통해 최신 설정을 재확인해 주세요:
frontend/src/utils/dynamicPath.ts (1)
1-4: 하드코딩된 경로는 확장성/재현성이 떨어집니다. 환경변수 기반 + 명시적 타입을 사용하세요.현재 특정 클럽 ID가 고정값으로 들어가 있어(과거 러닝: AdminPage의 하드코딩 ID 제거 예정) 규모 확장과 환경별 재현이 어렵습니다. CI/CD에서 제공하는 ID 목록 또는 스냅샷 파일을 읽어 경로를 생성하는 방식이 바람직합니다.
-const getDynamicPaths = () => { - return ['/', '/club/68b90ee402ba2206f54a2e5c']; -}; -export default getDynamicPaths; +const DEFAULT_PRERENDER_PATHS = ['/'] as const; + +// CI나 배포 환경에서 콤마로 구분된 ID 목록을 주입: PRERENDER_CLUB_IDS=abc,def,ghi +const clubIds = + (process.env.PRERENDER_CLUB_IDS ?? '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + +const getDynamicPaths = (): string[] => { + const clubPaths = clubIds.map((id) => `/club/${id}`); + return [...DEFAULT_PRERENDER_PATHS, ...clubPaths]; +}; + +export default getDynamicPaths;검증 요청:
- PR 환경/배포 파이프라인에 PRERENDER_CLUB_IDS를 주입하도록 설정되어 있는지 확인 부탁드립니다. 없으면 임시로 소수의 ID만 넣고 점진 확장해도 됩니다.
frontend/config/webpack.common.ts (1)
8-9: Prerenderer 구성 오류: renderer에 문자열을 전달하고 있습니다. 인스턴스로 교체 + 프로덕션 한정 실행이 필요합니다.webpack-plugin은 renderer 인스턴스를 기대합니다. 또한 개발 번들에 프리렌더를 항상 수행하면 매우 느려집니다. 프로덕션에서만 동작하도록 가드하고, 데이터 준비 이벤트를 기다리도록 설정해 주세요.
-const PrerendererWebpackPlugin = require('@prerenderer/webpack-plugin'); +const PrerendererWebpackPlugin = require('@prerenderer/webpack-plugin'); +const PuppeteerRenderer = require('@prerenderer/renderer-puppeteer'); +const isProd = process.env.NODE_ENV === 'production'; @@ - new ForkTsCheckerWebpackPlugin(), - new PrerendererWebpackPlugin({ - routes: getDynamicPaths(), - renderer: '@prerenderer/renderer-puppeteer', - }), + new ForkTsCheckerWebpackPlugin(), + ...(isProd + ? [ + new PrerendererWebpackPlugin({ + routes: getDynamicPaths(), + renderer: new PuppeteerRenderer({ + renderAfterDocumentEvent: 'moadong:prerender-ready', + maxConcurrentRoutes: 4, + // 필요 시 headless 옵션/timeout 등 추가 + }), + }), + ] + : []),검증 요청:
- 현재 설정으로 빌드 시 프리렌더 단계가 실제로 실행되는지, dist에 경로별 HTML이 생성되는지 확인 부탁드립니다.
- 이벤트명이 ClubDetailPage에서 디스패치한 것과 일치하는지 확인해 주세요.
Also applies to: 67-71
| <> | ||
| <title>{clubDetail.name}</title> | ||
| {showHeader && <Header />} |
There was a problem hiding this comment.
동적 OG 메타 태그가 누락되어 목표를 달성하지 못합니다.
title만으로는 미리보기/공유 썸네일이 생성되지 않습니다. React 19 Document Metadata를 활용해 OG/Twitter 메타를 함께 출력해 주세요. 또한 프리렌더 타이밍을 맞추기 위해 데이터 로드 완료 시 커스텀 이벤트를 발생시키는 것이 안전합니다.
다음 패치를 적용해 주세요(절대 URL은 환경변수 사용 권장):
@@
return (
<>
- <title>{clubDetail.name}</title>
+ <title>{clubDetail.name} | MOA</title>
+ <meta property="og:title" content={`${clubDetail.name} | MOA`} />
+ <meta property="og:type" content="website" />
+ <meta property="og:url" content={`${process.env.SITE_ORIGIN}/club/${clubId}`} />
+ <meta property="og:image" content={`${process.env.OG_IMAGE_BASE_URL}/club/${clubId}`} />
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta name="twitter:image" content={`${process.env.OG_IMAGE_BASE_URL}/club/${clubId}`} />
{showHeader && <Header />}추가로, 데이터 준비 완료 후 프리렌더 신호를 보내세요(파일 내 적절한 useEffect에 배치):
useEffect(() => {
if (clubDetail) {
window.dispatchEvent(new Event('moadong:prerender-ready'));
}
}, [clubDetail]);🤖 Prompt for AI Agents
In frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx around lines 45-47, the
component currently only renders a <title> tag which prevents dynamic OG/Twitter
metadata from being emitted; update the file to use React 19 Document Metadata
APIs to output full meta tags for og:title, og:description, og:image, og:url and
twitter:card/twitter:title/twitter:description/twitter:image (use an absolute
URL for the image and page URL sourced from an environment variable), and ensure
these meta values derive from clubDetail; additionally, add a useEffect that
listens for when clubDetail is loaded and then dispatches
window.dispatchEvent(new Event('moadong:prerender-ready')) so the pre-renderer
gets the ready signal.
5a15464 to
2f438a1
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
frontend/src/utils/initSDK.ts (1)
20-25: 클라이언트 코드에서process.env사용 → Vite 환경에서는 런타임 undefined 가능Channel, Sentry, Kakao 설정이
process.env를 참조합니다. Vite에서는 클라이언트 번들에서process.env.*가 주입되지 않으므로import.meta.env.VITE_*로 통일하세요. 또한 SSR 환경을 대비해 window 의존 코드는 클라이언트에서만 실행되도록 방어를 추가하는 것이 안전합니다.export function initializeChannelService() { - ChannelService.loadScript(); - if (process.env.CHANNEL_PLUGIN_KEY) { + ChannelService.loadScript(); + const channelKey = import.meta.env.VITE_CHANNEL_PLUGIN_KEY; + if (channelKey) { ChannelService.boot({ - pluginKey: process.env.CHANNEL_PLUGIN_KEY, + pluginKey: channelKey, }); } } export function initializeSentry() { - if (process.env.NODE_ENV === 'development') { + if (import.meta.env.MODE === 'development') { return; } Sentry.init({ - dsn: process.env.SENTRY_DSN, + dsn: import.meta.env.VITE_SENTRY_DSN, sendDefaultPii: false, - release: process.env.SENTRY_RELEASE, + release: import.meta.env.VITE_SENTRY_RELEASE, tracesSampleRate: 0.1, }); } export function initializeKakaoSDK() { - if (!process.env.KAKAO_JAVASCRIPT_KEY) { + const kakaoKey = import.meta.env.VITE_KAKAO_JAVASCRIPT_KEY; + if (!kakaoKey) { console.warn('환경변수가 설정되어 있지 않습니다.'); return; } if (!window.Kakao) { console.error('카카오 SDK가 로드되지 않았습니다.'); return; } try { - window.Kakao.init(`${process.env.KAKAO_JAVASCRIPT_KEY}`); + window.Kakao.init(kakaoKey); } catch (error) { console.error('카카오 SDK 초기화에 실패했습니다:', error); } }추가 권장:
- 이 초기화 함수들은 반드시 클라이언트에서만 호출되도록 호출 지점을
if (typeof window !== 'undefined')가드 하거나, 클라이언트 렌더러(+onRenderClient)에서만 실행하세요.Also applies to: 28-38, 41-56
frontend/src/pages/MainPage/components/Banner/Banner.tsx (3)
24-26: 빈 배너 배열 처리 누락 — 런타임 오류 위험banners가 빈 배열이면 extendedBanners에 undefined가 들어가 이미지 렌더 시 오류가 납니다. 안전 가드가 필요합니다.
컴포넌트 초반에 가드를 추가하세요(파일 외 변경 예시):
const banners = isMobile ? mobileBanners : desktopBanners; if (!banners || banners.length === 0) { return null; // 또는 스켈레톤/플레이스홀더 } const extendedBanners = [banners[banners.length - 1], ...banners, banners[0]];
41-51: 폼팩터 전환 시 인덱스 범위 보장 필요데스크톱↔모바일 전환으로 banners.length가 달라지면 currentSlideIndex가 범위를 벗어나 빈 화면으로 이동할 수 있습니다. 전환 시 클램프하세요.
파일 외 변경 예시(라인 57 이후에 추가):
useEffect(() => { // banners.length 변경 시 인덱스 보정 setCurrentSlideIndex((prev) => { if (banners.length === 0) return 0; if (prev < 1) return 1; if (prev > banners.length) return banners.length; return prev; }); setIsReady(false); setIsAnimating(false); updateSlideWidth(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [banners.length]); // updateSlideWidth는 useCallback 의존성에 이미 포함됨
109-114: 매직 넘버(3000) 상수화자동 슬라이드 인터벌 3000ms를 상수로 분리해 의도 명확화 및 재사용을 용이하게 해주세요. (가이드라인 준수)
파일 외 변경 예시:
// frontend/src/constants/responsive.ts export const SLIDE_INTERVAL_MS = 3000;// 본 파일 const interval = setInterval(() => { moveToNextSlide(); }, SLIDE_INTERVAL_MS);
🧹 Nitpick comments (8)
frontend/vite.config.ts (1)
10-11: styled-components SSR 일관성 확보를 위해 Babel 플러그인 활성화 권장서버/클라이언트 클래스명 불일치 방지를 위해
babel-plugin-styled-components를 React 플러그인에 연결하세요.- plugins: [react(), vike()], + plugins: [ + react({ + babel: { + plugins: [['babel-plugin-styled-components', { ssr: true, displayName: true, pure: true }]], + }, + }), + vike(), + ],패키지 누락 시
devDependencies에babel-plugin-styled-components추가가 필요합니다.frontend/src/pages/server/index.ts (1)
10-11: 매직 넘버(3000) 상수화가이드라인(frontend/**/*.{ts,tsx})에 따라 매직 넘버를 명명 상수로 분리해주세요.
As per coding guidelines
-function startServer() { +function startServer() { + const DEFAULT_PORT = 3000; const app = new Hono(); apply(app); - const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; + const port = process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_PORT; return serve(app, { port }); }frontend/src/hooks/useIsMobile.ts (2)
6-15: matchMedia 기반 구현 고려window.innerWidth 비교 대신 matchMedia('(max-width: Xpx)')는 미디어쿼리와 일관되고 변동(줌/디스플레이 설정)에 더 탄탄합니다. 선택 사항입니다.
예시:
useEffect(() => { const mql = window.matchMedia(`(max-width: ${MOBILE_MAX_WIDTH}px)`); const onChange = () => setIsMobile(mql.matches); onChange(); mql.addEventListener('change', onChange); return () => mql.removeEventListener('change', onChange); }, []);
13-15: 다중 사용 시 resize 리스너 중복 등록 가능성여러 컴포넌트에서 이 훅을 사용할 경우 리스너가 누적됩니다. 전역 single subscription 패턴(예: 전역 store/이벤트 버스 기반 useMediaQuery)로 개선을 고려해주세요.
frontend/src/pages/MainPage/components/Banner/Banner.tsx (4)
17-17: 모바일 판별 로직 중복 — useIsMobile 훅 재사용 권장동일 로직을 훅과 컴포넌트에 이중으로 두지 말고 훅으로 일원화하세요. 유지보수성/일관성 향상됩니다.
아래처럼 교체를 제안합니다:
- const [isMobile, setIsMobile] = useState(false); + const isMobile = useIsMobile();추가 임포트(파일 외 변경):
import useIsMobile from '@/hooks/useIsMobile';
120-126: 접근성: 버튼에 aria-label 추가 권장이미지 alt만으로는 버튼 의미가 충분치 않을 수 있습니다. 버튼 자체에 aria-label을 추가하세요.
예시:
<Styled.SlideButton onClick={moveToPrevSlide} aria-label="이전 배너"> <img src={SlideButton[0]} alt="Previous Slide" /> </Styled.SlideButton>
27-39: ResizeObserver 활용으로 폭 계산 안정성 개선(선택)윈도우 리사이즈 외에도 컨테이너 레이아웃 변화에 대응하려면 ResizeObserver로 slideRef width를 관찰하는 것이 견고합니다.
예시:
useEffect(() => { if (!slideRef.current) return; const ro = new ResizeObserver(() => updateSlideWidth()); ro.observe(slideRef.current); return () => ro.disconnect(); }, [updateSlideWidth]);
41-48: 500px 임계값 상수화 및 중복 제거 useIsMobile.ts와 Banner.tsx에서 500px 하드코딩을 제거하고 공통 상수나 훅(useIsMobile)을 활용하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (15)
frontend/docs/vike-migration-strategy.md(1 hunks)frontend/package.json(2 hunks)frontend/src/App.tsx(2 hunks)frontend/src/constants/api.ts(1 hunks)frontend/src/hooks/useIsMobile.ts(1 hunks)frontend/src/pages/+Page.jsx(1 hunks)frontend/src/pages/+route.js(1 hunks)frontend/src/pages/MainPage/components/Banner/Banner.tsx(2 hunks)frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx(1 hunks)frontend/src/pages/renderer/+config.h.js(1 hunks)frontend/src/pages/renderer/+onRenderClient.jsx(1 hunks)frontend/src/pages/renderer/+onRenderHtml.jsx(1 hunks)frontend/src/pages/server/index.ts(1 hunks)frontend/src/utils/initSDK.ts(1 hunks)frontend/vite.config.ts(1 hunks)
✅ Files skipped from review due to trivial changes (4)
- frontend/src/pages/renderer/+config.h.js
- frontend/src/pages/+route.js
- frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx
- frontend/docs/vike-migration-strategy.md
🧰 Additional context used
📓 Path-based instructions (2)
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx}: Replace magic numbers with named constants for clarity.
Replace complex or nested ternary operators with if/else statements or IIFEs for readability.
Assign complex boolean conditions to named variables.
Use consistent return types for similar functions and hooks.
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle).
Use unique, descriptive names for custom wrappers and functions to avoid ambiguity.
Define constants near related logic or ensure names link them clearly.
Files:
frontend/src/hooks/useIsMobile.tsfrontend/src/constants/api.tsfrontend/src/pages/MainPage/components/Banner/Banner.tsxfrontend/src/pages/server/index.tsfrontend/vite.config.tsfrontend/src/utils/initSDK.tsfrontend/src/App.tsx
frontend/**/*.tsx
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.tsx: Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Separate significantly different conditional UI/logic into distinct components.
Colocate simple, localized logic or use inline definitions to reduce context switching.
Choose field-level or form-level cohesion based on form requirements.
Break down broad state management into smaller, focused hooks or contexts.
Use component composition instead of props drilling.
Files:
frontend/src/pages/MainPage/components/Banner/Banner.tsxfrontend/src/App.tsx
🧠 Learnings (3)
📚 Learning: 2025-05-16T06:25:11.193Z
Learnt from: seongwon030
PR: Moadong/moadong#418
File: frontend/src/mocks/api/clubHandlers.test.ts:0-0
Timestamp: 2025-05-16T06:25:11.193Z
Learning: API 관련 상수(예: API_BASE)는 frontend/src/mocks/constants/api.ts에 정의되어 있으며, 다른 파일에서 재정의하지 말고 이 파일에서 import하여 사용해야 합니다.
Applied to files:
frontend/src/constants/api.ts
📚 Learning: 2025-07-19T05:09:10.702Z
Learnt from: CR
PR: Moadong/moadong#0
File: frontend/.cursorrules:0-0
Timestamp: 2025-07-19T05:09:10.702Z
Learning: Applies to frontend/**/*.tsx : Abstract complex logic/interactions into dedicated components or higher-order components (HOCs).
Applied to files:
frontend/src/App.tsx
📚 Learning: 2025-07-19T05:09:10.702Z
Learnt from: CR
PR: Moadong/moadong#0
File: frontend/.cursorrules:0-0
Timestamp: 2025-07-19T05:09:10.702Z
Learning: Applies to frontend/**/*.tsx : Break down broad state management into smaller, focused hooks or contexts.
Applied to files:
frontend/src/App.tsx
🧬 Code graph analysis (2)
frontend/src/pages/renderer/+onRenderClient.jsx (2)
frontend/src/utils/initSDK.ts (4)
initializeMixpanel(5-16)initializeChannelService(18-25)initializeSentry(27-38)initializeKakaoSDK(40-56)frontend/src/pages/renderer/+onRenderHtml.jsx (1)
pageContext(9-9)
frontend/src/pages/renderer/+onRenderHtml.jsx (1)
frontend/src/pages/renderer/+onRenderClient.jsx (1)
pageContext(19-19)
🔇 Additional comments (8)
frontend/src/pages/+Page.jsx (1)
3-10: LGTMSSR 엔트리와의 경계가 명확하고, App 위임 구조가 단순합니다.
frontend/src/pages/server/index.ts (1)
5-5: default export가 즉시 서버를 기동 — 미의도 사이드이펙트 여부 확인모듈 import 시 서버가 곧바로 시작됩니다. 툴링(예: 테스트 러너, 스토리북)에서 해당 모듈을 import할 가능성이 있다면 지연 기동 형태(함수 export)로 전환을 고려하세요.
필요 시:
-export default startServer(); +export default startServer;(툴에서 명시적으로
startServer()호출)frontend/src/utils/initSDK.ts (1)
6-7: Vite env로의 전환 👍Mixpanel 토큰 접근을
import.meta.env로 바꾼 점 적절합니다.토큰 키가
.env.*에VITE_MIXPANEL_TOKEN으로 정의되어 있는지 확인해주세요.frontend/src/App.tsx (1)
28-81: BrowserRouter 적용 확인됨
frontend/src/pages/renderer/+onRenderClient.jsx에서 이미<BrowserRouter>로<Page>를 감싸고 있습니다. 추가 조치 불필요합니다.frontend/src/constants/api.ts (1)
1-2: 모킹 상수 파일(clubApi.ts)과 프로덕션 상수는 분리 유지
프로덕션 코드에서src/mocks/constants/api.ts는 존재하지 않으며, 모킹용src/mocks/constants/clubApi.ts를 재사용하면 안 됩니다. 프로덕션 SoT는src/constants/api.ts로 유지하세요.Likely an incorrect or invalid review comment.
frontend/package.json (1)
6-8: dev 스크립트를vike preview→vike dev로 수정- "dev": "vike preview", + "dev": "vike dev", "build": "vike build",
preview는 빌드 결과물을 필요로 하므로 로컬 개발 루프에는vike dev를 사용해야 합니다. 변경 후pnpm dev/npm run dev로 빌드 없이 서버가 실행되는지 확인하세요.frontend/src/hooks/useIsMobile.ts (1)
4-4: SSR 안전한 초기값 적용, 좋습니다초기값을 false로 두고 마운트 시점에 동기화하는 방식은 SSR/Hydration 경고를 예방합니다.
frontend/src/pages/MainPage/components/Banner/Banner.tsx (1)
53-56: cleanup 로직에 debounce.cancel 호출 누락return () => { window.removeEventListener('resize', handleResize); + handleResize.cancel?.(); };debounce 유틸의 cancel 지원 여부를 확인하고, 지원 시 cleanup에서 호출하세요.
| setIsMobile(window.innerWidth <= 500); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
매직 넘버(500) 상수화 및 공통화 필요
동일 임계값(500px)을 여러 곳에서 반복 사용하면 드리프트 위험이 있습니다. 공용 상수로 분리해 Banner 등과 공유해주세요. (가이드라인: frontend/**/* 매직 넘버 상수화)
아래처럼 대체를 권장합니다:
- setIsMobile(window.innerWidth <= 500);
+ setIsMobile(window.innerWidth <= MOBILE_MAX_WIDTH);상수/임포트 예시(파일 외 변경):
// frontend/src/constants/responsive.ts
export const MOBILE_MAX_WIDTH = 500;// 이 훅 파일 상단
import { MOBILE_MAX_WIDTH } from '@/constants/responsive';🤖 Prompt for AI Agents
In frontend/src/hooks/useIsMobile.ts around lines 8 to 9, the hook uses the
magic number 500 for the mobile width threshold; extract this value into a
shared constant (e.g., export const MOBILE_MAX_WIDTH = 500 in
frontend/src/constants/responsive.ts), import that constant at the top of this
hook, and replace window.innerWidth <= 500 with window.innerWidth <=
MOBILE_MAX_WIDTH so all components (Banner, etc.) can share the same threshold.
| useEffect(() => { | ||
| const checkIsMobile = () => window.innerWidth <= 500; | ||
| setIsMobile(checkIsMobile()); | ||
|
|
||
| updateSlideWidth(); | ||
| const handleResize = debounce(() => { | ||
| setIsMobile(window.innerWidth <= 500); | ||
| setIsMobile(checkIsMobile()); | ||
| setIsReady(false); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
매직 넘버 제거 + isMobile 직접 변경 제거
- 500/200 매직 넘버를 상수로 대체하세요. (가이드라인 준수)
- isMobile은 훅이 관리하므로 이 컴포넌트에서 setIsMobile 호출 제거가 안전합니다.
아래와 같이 단순화/상수화하세요:
useEffect(() => {
- const checkIsMobile = () => window.innerWidth <= 500;
- setIsMobile(checkIsMobile());
-
updateSlideWidth();
const handleResize = debounce(() => {
- setIsMobile(checkIsMobile());
setIsReady(false);
setIsAnimating(false);
updateSlideWidth();
- }, 200);
+ }, RESIZE_DEBOUNCE_MS);추가 상수/임포트(파일 외 변경):
// frontend/src/constants/responsive.ts
export const MOBILE_MAX_WIDTH = 500;
export const RESIZE_DEBOUNCE_MS = 200;// 상단
import { RESIZE_DEBOUNCE_MS } from '@/constants/responsive';추가로, isMobile 변경 시 슬라이드 폭/상태를 동기화하는 별도 effect를 두면 더 안전합니다(아래 별도 코멘트 참고).
Based on learnings
🤖 Prompt for AI Agents
In frontend/src/pages/MainPage/components/Banner/Banner.tsx around lines 41 to
48, remove the inline magic numbers and the local setIsMobile call: replace
numeric literals 500 and 200 with constants MOBILE_MAX_WIDTH and
RESIZE_DEBOUNCE_MS (imported from frontend/src/constants/responsive), stop
calling setIsMobile here (this component should not directly mutate the
hook-managed isMobile), and keep updateSlideWidth() and setIsReady(false) inside
the debounced resize handler; additionally add a separate effect (outside this
change) to synchronize slide width/state when isMobile changes.
| initializeMixpanel(); | ||
| initializeChannelService(); | ||
| initializeSentry(); | ||
| initializeKakaoSDK(); |
There was a problem hiding this comment.
즉시 실행되는 SDK 초기화에서 process.env 참조로 런타임 크래시 발생
현재 initializeChannelService(), initializeSentry(), initializeKakaoSDK()는 process.env.*를 읽습니다(참고: frontend/src/utils/initSDK.ts Line 17-55). Vite 클라이언트 번들에는 process가 기본으로 주입되지 않으므로, 이 줄들이 실행되는 즉시 ReferenceError: process is not defined가 발생합니다. 하이드레이션 진입 시점에 앱 전체가 중단되는 치명적 버그이니, 반드시 Vite 스타일(import.meta.env)로 이관해 주세요.
다음과 같이 initSDK.ts를 수정하면 문제를 방지할 수 있습니다:
export function initializeChannelService() {
ChannelService.loadScript();
- if (process.env.CHANNEL_PLUGIN_KEY) {
+ if (import.meta.env.VITE_CHANNEL_PLUGIN_KEY) {
ChannelService.boot({
- pluginKey: process.env.CHANNEL_PLUGIN_KEY,
+ pluginKey: import.meta.env.VITE_CHANNEL_PLUGIN_KEY,
});
}
}
export function initializeSentry() {
- if (process.env.NODE_ENV === 'development') {
+ if (import.meta.env.DEV) {
return;
}
Sentry.init({
- dsn: process.env.SENTRY_DSN,
+ dsn: import.meta.env.VITE_SENTRY_DSN,
sendDefaultPii: false,
- release: process.env.SENTRY_RELEASE,
+ release: import.meta.env.VITE_SENTRY_RELEASE,
tracesSampleRate: 0.1,
});
}
export function initializeKakaoSDK() {
- if (!process.env.KAKAO_JAVASCRIPT_KEY) {
+ if (!import.meta.env.VITE_KAKAO_JAVASCRIPT_KEY) {
console.warn('환경변수가 설정되어 있지 않습니다.');
return;
}
if (!window.Kakao) {
console.error('카카오 SDK가 로드되지 않았습니다.');
return;
}
try {
- window.Kakao.init(`${process.env.KAKAO_JAVASCRIPT_KEY}`);
+ window.Kakao.init(`${import.meta.env.VITE_KAKAO_JAVASCRIPT_KEY}`);
} catch (error) {
console.error('카카오 SDK 초기화에 실패했습니다:', error);
}
}필요에 따라 실제 환경변수 명칭(VITE_ 접두사 등)을 프로젝트 규칙에 맞춰 조정해 주세요.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In frontend/src/pages/renderer/+onRenderClient.jsx around lines 13-16, the
immediate SDK initialization calls trigger a runtime ReferenceError because the
underlying init functions read process.env at module evaluation time; update
frontend/src/utils/initSDK.ts (lines ~17-55) to stop using process.env and
instead read environment values from import.meta.env (or a VITE_ prefixed var
per project convention), and ensure any env lookups occur inside the exported
initialize... functions (or after a typeof window check) so no process-dependent
lookup runs at module import time in the Vite client bundle.
|
|
||
| import React from 'react'; | ||
| import { renderToString } from 'react-dom/server'; | ||
| import { StaticRouter } from 'react-router'; |
There was a problem hiding this comment.
StaticRouter import 경로 오류(react-router v7) — SSR 실패 가능
StaticRouter는 react-router-dom/server에서 import해야 합니다.
-import { StaticRouter } from 'react-router';
+import { StaticRouter } from 'react-router-dom/server';추가로, 클라이언트 측은 <BrowserRouter>로 감싸져야 합니다(별도 코멘트 참조).
📝 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.
| import { StaticRouter } from 'react-router'; | |
| import { StaticRouter } from 'react-router-dom/server'; |
🤖 Prompt for AI Agents
In frontend/src/pages/renderer/+onRenderHtml.jsx around line 5, the StaticRouter
is imported from the wrong package; replace the import source 'react-router'
with 'react-router-dom/server' so StaticRouter is imported from
react-router-dom/server for SSR, and ensure the client-side app is wrapped with
BrowserRouter instead (adjust client entry accordingly).
| return escapeInject`<!DOCTYPE html> | ||
| <html> | ||
| <body> | ||
| <div id="root">${dangerouslySkipEscape(pageHtml)}</div> | ||
| </body> | ||
| </html>`; |
There was a problem hiding this comment.
동적 OG 메타 누락 — PR 목적(MOA-260)과 불일치
SSR HTML에 <head>와 OG/Twitter 메타가 없습니다. pageContext에서 문서 정보를 받아 동적으로 주입하세요(클럽별 타이틀/이미지).
- return escapeInject`<!DOCTYPE html>
- <html>
- <body>
- <div id="root">${dangerouslySkipEscape(pageHtml)}</div>
- </body>
- </html>`;
+ const { exports: { documentProps } = {} } = pageContext;
+ const title = documentProps?.title ?? 'MOA';
+ const description = documentProps?.description ?? '동아리 정보를 한 곳에서, MOA';
+ const ogImage = documentProps?.ogImage ?? '/og-default.png'; // 클럽별 생성 이미지 URL을 주입
+ return escapeInject`<!DOCTYPE html>
+ <html lang="ko">
+ <head>
+ <meta charSet="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>${title}</title>
+ <meta name="description" content="${description}" />
+ <meta property="og:title" content="${title}" />
+ <meta property="og:description" content="${description}" />
+ <meta property="og:image" content="${ogImage}" />
+ <meta property="og:type" content="website" />
+ <meta property="og:url" content="${urlPathname}" />
+ <meta name="twitter:card" content="summary_large_image" />
+ </head>
+ <body>
+ <div id="root">${dangerouslySkipEscape(pageHtml)}</div>
+ </body>
+ </html>`;클럽별 OG 이미지 자체 생성(동적 이미지 렌더링)이 필요하면 Hono에 GET /og/club/:clubId 라우트를 추가해 satori/resvg 등으로 PNG/SVG를 생성 후 ogImage에 해당 URL을 넣는 방식이 일반적입니다. 원하시면 초안 구현 도와드리겠습니다.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In frontend/src/pages/renderer/+onRenderHtml.jsx around lines 16-21, the SSR
HTML currently returns only a body and is missing a <head> with dynamic
OG/Twitter meta — use pageContext (e.g., pageContext.documentProps or
pageContext.title/description/ogImage/clubId) to inject <head> including
<title>, <meta name="description">, <meta property="og:title">, <meta
property="og:description">, <meta property="og:image"> and Twitter equivalents;
build og:image from pageContext.ogImage if provided or fall back to a generated
route like /og/club/${clubId} (implementing GET /og/club/:clubId separately with
satori/resvg if needed), ensure values are escaped when interpolating into
escapeInject and keep the existing dangerouslySkipEscape(pageHtml) for the root
div.
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
frontend/package.json(2 hunks)
| "dev": "vike preview", | ||
| "build": "vike build", | ||
| "build:dev": "vike build", | ||
| "build:prod": "vike build", |
There was a problem hiding this comment.
dev 스크립트가 미리보기 명령으로 바뀌면서 개발 서버를 띄울 수 없습니다.
vike preview는 선행 빌드 아티팩트를 요구하는 프로덕션 미리보기 용도라서, 지금처럼 개발자가 pnpm dev로 즉시 개발 서버를 띄우려 하면 dist가 없어 바로 실패합니다. 즉시 vike dev로 복원해야 합니다.
다음과 같이 수정해주세요:
- "dev": "vike preview",
+ "dev": "vike dev",📝 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.
| "dev": "vike preview", | |
| "build": "vike build", | |
| "build:dev": "vike build", | |
| "build:prod": "vike build", | |
| "dev": "vike dev", | |
| "build": "vike build", | |
| "build:dev": "vike build", | |
| "build:prod": "vike build", |
🤖 Prompt for AI Agents
In frontend/package.json around lines 6 to 9 the "dev" script currently runs
"vike preview" which requires prior build artifacts and prevents starting a live
dev server; change the "dev" script to "vike dev" so pnpm dev launches the
development server immediately; keep other build scripts as-is and ensure no
other tooling expects "dev" to run preview.
#️⃣연관된 이슈
#757
📝작업 내용
테스트 pr
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit