fix: harden Next 16 build pipeline#37
Conversation
- Migrate middleware entrypoint to proxy for Next.js 16 - Apply low-memory build env in GitHub Actions build steps - Split gallery page client into smaller focused components - Add trusted-types definitions required by DOMPurify types
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 44 minutes and 43 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Note
|
| Layer / File(s) | Summary |
|---|---|
Pagination Data Type and Component Exports components/gallery/GalleryPageParts.tsx |
PAGE_SIZE, GalleryPaginationData 및 카테고리 헬퍼를 정의하고 내보냅니다. |
Reusable Gallery UI Components components/gallery/GalleryPageParts.tsx |
FilterBar(디바운스 검색, 연도/카테고리 선택), GalleryCard(섬네일 상태, 키보드 접근성), Lightbox(키보드 내비, 스크롤 잠금), GalleryPagination(말줄임 페이징 윈도잉), SkeletonGrid를 구현해 내보냅니다. |
GalleryPageClient Integration and Refactoring components/gallery/GalleryPageClient.tsx |
파일 내부 UI를 GalleryPageParts로 대체하고 API 응답의 pagination을 GalleryPaginationData로 사용하도록 조정하며, 필터 변경 시 page 리셋 로직과 GalleryPagination을 사용한 페이지 전환을 구현합니다. |
Build Infrastructure and Configuration
| Layer / File(s) | Summary |
|---|---|
Low-Memory Build Configuration .github/workflows/ci.yml, next.config.js, package.json |
CI 빌드·배포 단계에 CI_LOW_MEMORY_BUILD와 NODE_OPTIONS 환경 변수를 주입하고, next.config.js에서 CI_LOW_MEMORY_BUILD 기반의 lowMemoryBuild 플래그로 실험적 빌드/워커/메모리 설정을 조건부 적용합니다. @types/trusted-types가 devDependencies에 추가되었습니다. |
Clerk Middleware Export Refactoring proxy.ts |
CSRF Origin 검사 로직을 enforceOrigin으로 분리하고, E2E 환경에서는 origin 검사만 수행하는 e2eProxy를 도입하며 proxy를 명명 내보내기로 전환합니다. |
🎯 3 (Moderate) | ⏱️ ~25 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | PR 제목은 Next.js 16 빌드 파이프라인 강화에 관한 주요 변경사항을 명확하게 요약하고 있으며, 실제 변경 내용(CI 빌드 환경변수 추가, 미들웨어 엔트포인트 변경, 갤러리 컴포넌트 분할)과 일치합니다. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
fix/next16-build-hardening
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: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/gallery/GalleryPageClient.tsx (2)
30-31:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
page쿼리를 정수/최솟값으로 정규화하지 않아NaN이 전파됩니다.잘못된 URL(
?page=abc,?page=0)에서 페이지네이션 계산과 API 요청 파라미터가 비정상 상태가 됩니다.수정 제안
- const page = Number(searchParams.get('page') ?? '1') + const rawPage = Number(searchParams.get('page') ?? '1') + const page = Number.isFinite(rawPage) && rawPage >= 1 ? Math.floor(rawPage) : 1🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/gallery/GalleryPageClient.tsx` around lines 30 - 31, Normalize the page query parsing in GalleryPageClient.tsx: parse searchParams.get('page') into an integer, default to 1 on missing/invalid/NaN, and clamp to a minimum of 1 before using it in pagination calculations and API requests; update the code around the page constant (where page is currently computed from searchParams) to use parseInt/Number validation and min(…,1) behavior so '?page=abc' or '?page=0' yield page = 1.
72-77:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHTTP 실패 응답을 성공 데이터로 캐스팅해 런타임 오류로 이어질 수 있습니다.
현재는
r.ok검사 없이ApiResponse로 단정해, 에러 JSON/비정상 JSON에서도data.items.length접근 경로가 열려 있습니다.수정 제안
fetch(`/api/gallery?${qs}`, { signal: ctrl.signal }) - .then(r => r.json()) - .then((json: ApiResponse) => { + .then(async r => { + const json: unknown = await r.json() + if (!r.ok) throw new Error('gallery_fetch_failed') + if ( + !json || + typeof json !== 'object' || + !Array.isArray((json as { items?: unknown }).items) || + !('pagination' in json) + ) { + throw new Error('gallery_payload_invalid') + } + return json as ApiResponse + }) + .then(json => { setData(json) setLoading(false) })Also applies to: 147-170
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/gallery/GalleryPageClient.tsx` around lines 72 - 77, The fetch response is being unconditionally cast to ApiResponse and assigned via setData without checking r.ok, which can lead to runtime errors when the server returns an error payload; update the fetch handling (the fetch(...) promise chain used near setData and setLoading and the similar block around lines 147-170) to first check response.ok after receiving r, parse the JSON, and if !r.ok either throw a controlled Error or map the error shape to a safe object before calling setData, and add a .catch to set an error state and ensure setLoading(false) is always executed (e.g., in finally or both then/catch), so ApiResponse consumers never assume data.items exists on error responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 114-116: The CI workflow currently forces
NODE_OPTIONS='--max-old-space-size=1024' (and sets CI_LOW_MEMORY_BUILD='1'),
which may be too low for production builds; update the workflow to make the heap
limit configurable and raise the default: replace the hardcoded NODE_OPTIONS
with a variable (e.g., use NODE_OPTIONS: '${{ env.NODE_OPTIONS_OVERRIDE ||
"--max-old-space-size=2048" }}' or similar) and document CI_LOW_MEMORY_BUILD
usage, or increase the default to a safer value like 2048; ensure the pipeline
reads an optional override (env var) so you can adjust without changing the file
and add a short comment explaining how to tune NODE_OPTIONS for large builds.
In `@components/gallery/GalleryPageParts.tsx`:
- Around line 38-445: Add JSDoc comments above each exported component
(FilterBar, GalleryCard, Lightbox, GalleryPagination, SkeletonGrid) describing
the component purpose, summary of props (names and types/shape e.g.,
availableYears:number[], categories:GalleryCategory[], item:GalleryItem,
items:GalleryItem[], pagination:GalleryPaginationData,
onChange/onClick/onClose/onNavigate/onPage callbacks), and any important
behavior (debounce in FilterBar, keyboard handling in Lightbox, image
loading/error states in GalleryCard, pagination logic in GalleryPagination,
skeleton count via PAGE_SIZE). Use standard /** ... */ block comments directly
above each function so IDEs pick up the types and prop docs; keep brief one-line
summary, `@param` entries for each prop and `@returns` description for the rendered
JSX.
- Around line 56-68: The debounce timer created in handleSearch (stored in
debounceRef) isn't cleared on unmount, allowing a pending setTimeout to run
after the component is unmounted; add a useEffect cleanup that clears
debounceRef.current (using clearTimeout) when the component unmounts to cancel
any pending onChange calls; reference debounceRef and handleSearch so you clear
debounceRef.current in the effect cleanup to prevent delayed routing/update
after unmount.
- Around line 246-257: The useEffect handler currently overwrites
document.body.style.overflow and restores it to '' on cleanup, which can lose a
prior value; modify the effect in GalleryPageParts.tsx so it captures the
previousOverflow (e.g. const prevOverflow = document.body.style.overflow) before
setting overflow = 'hidden', and in the cleanup restore
document.body.style.overflow = prevOverflow; keep the existing keyboard handler
registration (handler) and removals
(document.addEventListener/removeEventListener) and ensure onClose and
onNavigate usages are unchanged.
In `@next.config.js`:
- Around line 72-111: The review incorrectly asserts that
experimental.optimizeCss:false and gzipSize:false in the lowMemoryBuild branch
increase bundle size; update the config and comments to reflect Next.js 16.2.7
behavior: keep the lowMemoryBuild object as-is but remove or correct any
comment/assumption that “optimizeCss: false” increases bundle size (note that
optimizeCss is Pages Router / critters-specific) and that gzipSize toggles
on-disk bundle size (explain gzipSize is for reporting, actual transport gzip is
handled by hosting), and add a TODO to run measured build size comparisons if
the trade-off needs validation; reference the experimental.lowMemoryBuild branch
and keys experimental.optimizeCss and experimental.gzipSize when making these
changes.
---
Outside diff comments:
In `@components/gallery/GalleryPageClient.tsx`:
- Around line 30-31: Normalize the page query parsing in GalleryPageClient.tsx:
parse searchParams.get('page') into an integer, default to 1 on
missing/invalid/NaN, and clamp to a minimum of 1 before using it in pagination
calculations and API requests; update the code around the page constant (where
page is currently computed from searchParams) to use parseInt/Number validation
and min(…,1) behavior so '?page=abc' or '?page=0' yield page = 1.
- Around line 72-77: The fetch response is being unconditionally cast to
ApiResponse and assigned via setData without checking r.ok, which can lead to
runtime errors when the server returns an error payload; update the fetch
handling (the fetch(...) promise chain used near setData and setLoading and the
similar block around lines 147-170) to first check response.ok after receiving
r, parse the JSON, and if !r.ok either throw a controlled Error or map the error
shape to a safe object before calling setData, and add a .catch to set an error
state and ensure setLoading(false) is always executed (e.g., in finally or both
then/catch), so ApiResponse consumers never assume data.items exists on error
responses.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5bfd36cd-d38f-46c0-bdaa-799f7188808d
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (6)
.github/workflows/ci.ymlcomponents/gallery/GalleryPageClient.tsxcomponents/gallery/GalleryPageParts.tsxnext.config.jspackage.jsonproxy.ts
| export function FilterBar({ | ||
| availableYears, | ||
| categories, | ||
| year, | ||
| category, | ||
| search, | ||
| totalCount, | ||
| onChange, | ||
| }: { | ||
| availableYears: number[] | ||
| categories: GalleryCategory[] | ||
| year: string | ||
| category: string | ||
| search: string | ||
| totalCount: number | ||
| onChange: (updates: Record<string, string>) => void | ||
| }) { | ||
| const [localSearch, setLocalSearch] = useState(search) | ||
| const debounceRef = useRef<NodeJS.Timeout | null>(null) | ||
|
|
||
| useEffect(() => { | ||
| setLocalSearch(search) | ||
| }, [search]) | ||
|
|
||
| const handleSearch = (v: string) => { | ||
| setLocalSearch(v) | ||
| if (debounceRef.current) clearTimeout(debounceRef.current) | ||
| debounceRef.current = setTimeout(() => { | ||
| onChange({ search: v }) | ||
| }, 400) | ||
| } | ||
|
|
||
| return ( | ||
| <div className='space-y-5'> | ||
| <div className='relative max-w-lg mx-auto'> | ||
| <span className='absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none'> | ||
| <svg className='w-4 h-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'> | ||
| <path | ||
| strokeLinecap='round' | ||
| strokeLinejoin='round' | ||
| strokeWidth={2} | ||
| d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' | ||
| /> | ||
| </svg> | ||
| </span> | ||
| <input | ||
| id='gallery-search' | ||
| type='search' | ||
| value={localSearch} | ||
| onChange={e => handleSearch(e.target.value)} | ||
| placeholder='제목, 설명, 태그로 검색...' | ||
| className='w-full pl-10 pr-4 py-2.5 bg-background border border-border rounded-lg text-sm placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition' | ||
| /> | ||
| {localSearch && ( | ||
| <button | ||
| onClick={() => handleSearch('')} | ||
| className='absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground' | ||
| aria-label='검색어 지우기' | ||
| > | ||
| <svg className='w-4 h-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'> | ||
| <path | ||
| strokeLinecap='round' | ||
| strokeLinejoin='round' | ||
| strokeWidth={2} | ||
| d='M6 18L18 6M6 6l12 12' | ||
| /> | ||
| </svg> | ||
| </button> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className='flex flex-wrap justify-center gap-2'> | ||
| {(['all', ...availableYears.map(String)] as string[]).map(y => ( | ||
| <button | ||
| key={y} | ||
| onClick={() => onChange({ year: y })} | ||
| className={`px-4 py-1.5 rounded-full text-xs font-semibold border transition-all ${ | ||
| year === y | ||
| ? 'bg-primary text-primary-foreground border-primary' | ||
| : 'bg-background text-foreground border-border hover:border-primary/50' | ||
| }`} | ||
| > | ||
| {y === 'all' ? '전체 연도' : `${y}년`} | ||
| </button> | ||
| ))} | ||
| </div> | ||
|
|
||
| <div className='flex flex-wrap justify-center gap-2' role='group' aria-label='카테고리 필터'> | ||
| <button | ||
| onClick={() => onChange({ category: 'all' })} | ||
| className={`px-4 py-1.5 rounded-full text-xs font-semibold border transition-all ${ | ||
| category === 'all' | ||
| ? 'bg-accent text-accent-foreground border-accent' | ||
| : 'bg-background text-foreground border-border hover:border-accent/60' | ||
| }`} | ||
| > | ||
| ✨ 전체 | ||
| </button> | ||
| {categories.map(cat => ( | ||
| <button | ||
| key={cat.id} | ||
| onClick={() => onChange({ category: cat.id })} | ||
| className={`px-4 py-1.5 rounded-full text-xs font-semibold border transition-all ${ | ||
| category === cat.id | ||
| ? 'bg-accent text-accent-foreground border-accent' | ||
| : 'bg-background text-foreground border-border hover:border-accent/60' | ||
| }`} | ||
| > | ||
| {cat.icon} {cat.name} | ||
| <span className='ml-1 opacity-60'>({cat.count})</span> | ||
| </button> | ||
| ))} | ||
| </div> | ||
|
|
||
| <p className='text-center text-sm text-muted-foreground' role='status' aria-live='polite'> | ||
| {totalCount > 0 ? ( | ||
| <> | ||
| <strong className='text-foreground'>{totalCount}</strong>개의 사진 | ||
| </> | ||
| ) : ( | ||
| '검색 결과가 없습니다' | ||
| )} | ||
| </p> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| const ASPECT_RATIOS = ['aspect-square', 'aspect-[4/5]', 'aspect-[3/4]', 'aspect-[5/4]'] | ||
|
|
||
| export function GalleryCard({ | ||
| item, | ||
| index, | ||
| onClick, | ||
| }: { | ||
| item: GalleryItem | ||
| index: number | ||
| onClick: (item: GalleryItem) => void | ||
| }) { | ||
| const [loaded, setLoaded] = useState(false) | ||
| const [errored, setErrored] = useState(false) | ||
| const ratio = ASPECT_RATIOS[index % ASPECT_RATIOS.length] | ||
|
|
||
| return ( | ||
| <motion.div | ||
| layout | ||
| initial={{ opacity: 0, y: 20 }} | ||
| animate={{ opacity: 1, y: 0 }} | ||
| transition={{ duration: 0.35, delay: Math.min(index * 0.04, 0.6) }} | ||
| className='group cursor-pointer' | ||
| onClick={() => onClick(item)} | ||
| role='button' | ||
| tabIndex={0} | ||
| aria-label={`${item.title} 보기`} | ||
| onKeyDown={e => { | ||
| if (e.key === 'Enter' || e.key === ' ') { | ||
| e.preventDefault() | ||
| onClick(item) | ||
| } | ||
| }} | ||
| > | ||
| <div className={`relative ${ratio} bg-muted rounded-xl overflow-hidden`}> | ||
| {!loaded && !errored && ( | ||
| <div className='absolute inset-0 animate-pulse bg-gradient-to-r from-muted via-muted-foreground/10 to-muted' /> | ||
| )} | ||
| {errored && ( | ||
| <div className='absolute inset-0 flex items-center justify-center text-muted-foreground text-xs'> | ||
| 이미지 없음 | ||
| </div> | ||
| )} | ||
| <Image | ||
| src={item.thumbnail || item.src} | ||
| alt={`${item.title} - ${item.description}`} | ||
| fill | ||
| className={`object-cover transition-all duration-500 group-hover:scale-105 ${loaded ? 'opacity-100' : 'opacity-0'}`} | ||
| sizes='(max-width: 640px) 90vw, (max-width: 1024px) 45vw, 30vw' | ||
| quality={80} | ||
| loading={index < 8 ? 'eager' : 'lazy'} | ||
| onLoad={() => setLoaded(true)} | ||
| onError={() => setErrored(true)} | ||
| /> | ||
| <div className='absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300' /> | ||
| <div className='absolute top-2 left-2'> | ||
| <span className='text-xs px-2 py-0.5 bg-background/90 backdrop-blur-sm rounded-full font-medium text-foreground'> | ||
| {catIcon(item.category)} {catName(item.category)} | ||
| </span> | ||
| </div> | ||
| <div className='absolute bottom-0 left-0 right-0 p-3 translate-y-1 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300'> | ||
| <p className='text-white text-sm font-semibold line-clamp-2'>{item.title}</p> | ||
| {item.eventDate && <p className='text-white/70 text-xs mt-0.5'>{item.eventDate}</p>} | ||
| </div> | ||
| </div> | ||
| </motion.div> | ||
| ) | ||
| } | ||
|
|
||
| export function Lightbox({ | ||
| item, | ||
| items, | ||
| onClose, | ||
| onNavigate, | ||
| }: { | ||
| item: GalleryItem | ||
| items: GalleryItem[] | ||
| onClose: () => void | ||
| onNavigate: (dir: 'prev' | 'next') => void | ||
| }) { | ||
| const idx = items.findIndex(i => i.id === item.id) | ||
|
|
||
| useEffect(() => { | ||
| const handler = (e: KeyboardEvent) => { | ||
| if (e.key === 'Escape') onClose() | ||
| if (e.key === 'ArrowLeft') onNavigate('prev') | ||
| if (e.key === 'ArrowRight') onNavigate('next') | ||
| } | ||
| document.addEventListener('keydown', handler) | ||
| document.body.style.overflow = 'hidden' | ||
| return () => { | ||
| document.removeEventListener('keydown', handler) | ||
| document.body.style.overflow = '' | ||
| } | ||
| }, [onClose, onNavigate]) | ||
|
|
||
| return ( | ||
| <motion.div | ||
| role='dialog' | ||
| aria-modal='true' | ||
| aria-label={item.title} | ||
| initial={{ opacity: 0 }} | ||
| animate={{ opacity: 1 }} | ||
| exit={{ opacity: 0 }} | ||
| className='fixed inset-0 z-50 bg-black/95 backdrop-blur-sm flex items-center justify-center p-4' | ||
| onClick={onClose} | ||
| > | ||
| <motion.div | ||
| initial={{ scale: 0.9, opacity: 0 }} | ||
| animate={{ scale: 1, opacity: 1 }} | ||
| exit={{ scale: 0.9, opacity: 0 }} | ||
| transition={{ type: 'spring', damping: 28, stiffness: 240 }} | ||
| className='relative w-full max-w-5xl bg-card rounded-2xl overflow-hidden shadow-2xl' | ||
| onClick={e => e.stopPropagation()} | ||
| > | ||
| <div className='flex items-center justify-between p-4 border-b border-border'> | ||
| <div> | ||
| <h2 className='text-base font-semibold text-foreground line-clamp-1'>{item.title}</h2> | ||
| <p className='text-xs text-muted-foreground mt-0.5'> | ||
| {catIcon(item.category)} {catName(item.category)} | ||
| {item.eventDate && ` · ${item.eventDate}`} | ||
| </p> | ||
| </div> | ||
| <div className='flex items-center gap-2'> | ||
| <span className='text-xs text-muted-foreground'> | ||
| {idx + 1} / {items.length} | ||
| </span> | ||
| <button | ||
| onClick={onClose} | ||
| className='p-2 rounded-lg hover:bg-muted transition-colors text-muted-foreground hover:text-foreground' | ||
| aria-label='닫기' | ||
| > | ||
| <svg className='w-5 h-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'> | ||
| <path | ||
| strokeLinecap='round' | ||
| strokeLinejoin='round' | ||
| strokeWidth={2} | ||
| d='M6 18L18 6M6 6l12 12' | ||
| /> | ||
| </svg> | ||
| </button> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div | ||
| className='relative bg-black flex items-center justify-center' | ||
| style={{ minHeight: 400, maxHeight: '70vh' }} | ||
| > | ||
| <button | ||
| onClick={() => onNavigate('prev')} | ||
| className='absolute left-3 z-10 p-2 bg-background/60 backdrop-blur-sm rounded-full hover:bg-background/90 transition text-foreground' | ||
| aria-label='이전 이미지' | ||
| > | ||
| <svg className='w-5 h-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'> | ||
| <path | ||
| strokeLinecap='round' | ||
| strokeLinejoin='round' | ||
| strokeWidth={2} | ||
| d='M15 19l-7-7 7-7' | ||
| /> | ||
| </svg> | ||
| </button> | ||
| <Image | ||
| src={item.src} | ||
| alt={item.title} | ||
| width={1600} | ||
| height={1200} | ||
| className='max-w-full max-h-[70vh] object-contain' | ||
| quality={90} | ||
| priority | ||
| sizes='100vw' | ||
| /> | ||
| <button | ||
| onClick={() => onNavigate('next')} | ||
| className='absolute right-3 z-10 p-2 bg-background/60 backdrop-blur-sm rounded-full hover:bg-background/90 transition text-foreground' | ||
| aria-label='다음 이미지' | ||
| > | ||
| <svg className='w-5 h-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'> | ||
| <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M9 5l7 7-7 7' /> | ||
| </svg> | ||
| </button> | ||
| </div> | ||
|
|
||
| {item.tags.length > 0 && ( | ||
| <div className='flex flex-wrap gap-1.5 p-4 border-t border-border'> | ||
| {item.tags.slice(0, 8).map((tag, i) => ( | ||
| <span | ||
| key={i} | ||
| className='text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded-md' | ||
| > | ||
| #{tag} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </motion.div> | ||
|
|
||
| <div className='absolute bottom-4 left-1/2 -translate-x-1/2 text-white/40 text-xs'> | ||
| ESC 닫기 | ← → 이동 | ||
| </div> | ||
| </motion.div> | ||
| ) | ||
| } | ||
|
|
||
| export function GalleryPagination({ | ||
| pagination, | ||
| onPage, | ||
| }: { | ||
| pagination: GalleryPaginationData | ||
| onPage: (p: number) => void | ||
| }) { | ||
| const { page, totalPages } = pagination | ||
| if (totalPages <= 1) return null | ||
|
|
||
| const pages: (number | '…')[] = [] | ||
| if (totalPages <= 7) { | ||
| for (let i = 1; i <= totalPages; i++) pages.push(i) | ||
| } else { | ||
| pages.push(1) | ||
| if (page > 3) pages.push('…') | ||
| for (let i = Math.max(2, page - 1); i <= Math.min(totalPages - 1, page + 1); i++) { | ||
| pages.push(i) | ||
| } | ||
| if (page < totalPages - 2) pages.push('…') | ||
| pages.push(totalPages) | ||
| } | ||
|
|
||
| return ( | ||
| <nav aria-label='페이지 이동' className='flex items-center justify-center gap-1.5 mt-12'> | ||
| <button | ||
| onClick={() => onPage(page - 1)} | ||
| disabled={page === 1} | ||
| className='px-3 py-2 rounded-lg border border-border text-sm disabled:opacity-40 hover:bg-muted transition' | ||
| aria-label='이전 페이지' | ||
| > | ||
| ← | ||
| </button> | ||
| {pages.map((p, i) => | ||
| p === '…' ? ( | ||
| <span key={`ellipsis-${i}`} className='px-2 text-muted-foreground text-sm'> | ||
| … | ||
| </span> | ||
| ) : ( | ||
| <button | ||
| key={p} | ||
| onClick={() => onPage(p as number)} | ||
| aria-current={p === page ? 'page' : undefined} | ||
| className={`w-9 h-9 rounded-lg text-sm font-medium transition ${ | ||
| p === page | ||
| ? 'bg-primary text-primary-foreground' | ||
| : 'border border-border hover:bg-muted text-foreground' | ||
| }`} | ||
| > | ||
| {p} | ||
| </button> | ||
| ) | ||
| )} | ||
| <button | ||
| onClick={() => onPage(page + 1)} | ||
| disabled={page === totalPages} | ||
| className='px-3 py-2 rounded-lg border border-border text-sm disabled:opacity-40 hover:bg-muted transition' | ||
| aria-label='다음 페이지' | ||
| > | ||
| → | ||
| </button> | ||
| </nav> | ||
| ) | ||
| } | ||
|
|
||
| export function SkeletonGrid() { | ||
| return ( | ||
| <div className='columns-2 md:columns-3 lg:columns-4 gap-3'> | ||
| {Array.from({ length: PAGE_SIZE }).map((_, i) => ( | ||
| <div | ||
| key={i} | ||
| className='mb-3 break-inside-avoid rounded-xl bg-muted animate-pulse' | ||
| style={{ height: [200, 160, 220, 180][i % 4] }} | ||
| /> | ||
| ))} | ||
| </div> | ||
| ) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
공개 컴포넌트/함수에 JSDoc이 없어 IDE 인텔리센스 일관성이 떨어집니다.
FilterBar, GalleryCard, Lightbox, GalleryPagination, SkeletonGrid에 props/동작 요약 JSDoc을 추가해 공개 API를 문서화해 주세요.
As per coding guidelines, **/*.{ts,tsx}: Use JSDoc comments for functions and components to improve IDE intellisense.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/gallery/GalleryPageParts.tsx` around lines 38 - 445, Add JSDoc
comments above each exported component (FilterBar, GalleryCard, Lightbox,
GalleryPagination, SkeletonGrid) describing the component purpose, summary of
props (names and types/shape e.g., availableYears:number[],
categories:GalleryCategory[], item:GalleryItem, items:GalleryItem[],
pagination:GalleryPaginationData, onChange/onClick/onClose/onNavigate/onPage
callbacks), and any important behavior (debounce in FilterBar, keyboard handling
in Lightbox, image loading/error states in GalleryCard, pagination logic in
GalleryPagination, skeleton count via PAGE_SIZE). Use standard /** ... */ block
comments directly above each function so IDEs pick up the types and prop docs;
keep brief one-line summary, `@param` entries for each prop and `@returns`
description for the rendered JSX.
Source: Coding guidelines
| const debounceRef = useRef<NodeJS.Timeout | null>(null) | ||
|
|
||
| useEffect(() => { | ||
| setLocalSearch(search) | ||
| }, [search]) | ||
|
|
||
| const handleSearch = (v: string) => { | ||
| setLocalSearch(v) | ||
| if (debounceRef.current) clearTimeout(debounceRef.current) | ||
| debounceRef.current = setTimeout(() => { | ||
| onChange({ search: v }) | ||
| }, 400) | ||
| } |
There was a problem hiding this comment.
디바운스 타이머 정리 누락으로 언마운트 후 라우팅이 지연 실행될 수 있습니다.
setTimeout이 컴포넌트 언마운트 시 정리되지 않아, 페이지 전환 직후 이전 검색어 업데이트가 뒤늦게 반영될 수 있습니다.
수정 제안
const debounceRef = useRef<NodeJS.Timeout | null>(null)
+ useEffect(() => {
+ return () => {
+ if (debounceRef.current) clearTimeout(debounceRef.current)
+ }
+ }, [])
+
useEffect(() => {
setLocalSearch(search)
}, [search])📝 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.
| const debounceRef = useRef<NodeJS.Timeout | null>(null) | |
| useEffect(() => { | |
| setLocalSearch(search) | |
| }, [search]) | |
| const handleSearch = (v: string) => { | |
| setLocalSearch(v) | |
| if (debounceRef.current) clearTimeout(debounceRef.current) | |
| debounceRef.current = setTimeout(() => { | |
| onChange({ search: v }) | |
| }, 400) | |
| } | |
| const debounceRef = useRef<NodeJS.Timeout | null>(null) | |
| useEffect(() => { | |
| return () => { | |
| if (debounceRef.current) clearTimeout(debounceRef.current) | |
| } | |
| }, []) | |
| useEffect(() => { | |
| setLocalSearch(search) | |
| }, [search]) | |
| const handleSearch = (v: string) => { | |
| setLocalSearch(v) | |
| if (debounceRef.current) clearTimeout(debounceRef.current) | |
| debounceRef.current = setTimeout(() => { | |
| onChange({ search: v }) | |
| }, 400) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/gallery/GalleryPageParts.tsx` around lines 56 - 68, The debounce
timer created in handleSearch (stored in debounceRef) isn't cleared on unmount,
allowing a pending setTimeout to run after the component is unmounted; add a
useEffect cleanup that clears debounceRef.current (using clearTimeout) when the
component unmounts to cancel any pending onChange calls; reference debounceRef
and handleSearch so you clear debounceRef.current in the effect cleanup to
prevent delayed routing/update after unmount.
| useEffect(() => { | ||
| const handler = (e: KeyboardEvent) => { | ||
| if (e.key === 'Escape') onClose() | ||
| if (e.key === 'ArrowLeft') onNavigate('prev') | ||
| if (e.key === 'ArrowRight') onNavigate('next') | ||
| } | ||
| document.addEventListener('keydown', handler) | ||
| document.body.style.overflow = 'hidden' | ||
| return () => { | ||
| document.removeEventListener('keydown', handler) | ||
| document.body.style.overflow = '' | ||
| } |
There was a problem hiding this comment.
Lightbox가 기존 document.body.style.overflow 값을 덮어써서 복구를 깨뜨릴 수 있습니다.
정리 시 ''로 고정 복원하면, 모달 오픈 전 overflow 설정이 있던 페이지에서 상태를 잃습니다.
수정 제안
useEffect(() => {
+ const previousOverflow = document.body.style.overflow
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
if (e.key === 'ArrowLeft') onNavigate('prev')
if (e.key === 'ArrowRight') onNavigate('next')
}
document.addEventListener('keydown', handler)
document.body.style.overflow = 'hidden'
return () => {
document.removeEventListener('keydown', handler)
- document.body.style.overflow = ''
+ document.body.style.overflow = previousOverflow
}
}, [onClose, onNavigate])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/gallery/GalleryPageParts.tsx` around lines 246 - 257, The
useEffect handler currently overwrites document.body.style.overflow and restores
it to '' on cleanup, which can lose a prior value; modify the effect in
GalleryPageParts.tsx so it captures the previousOverflow (e.g. const
prevOverflow = document.body.style.overflow) before setting overflow = 'hidden',
and in the cleanup restore document.body.style.overflow = prevOverflow; keep the
existing keyboard handler registration (handler) and removals
(document.addEventListener/removeEventListener) and ensure onClose and
onNavigate usages are unchanged.
| experimental: lowMemoryBuild | ||
| ? { | ||
| cpus: 1, | ||
| memoryBasedWorkersCount: false, | ||
| webpackBuildWorker: false, | ||
| webpackMemoryOptimizations: true, | ||
| turbopackMemoryLimit: 1024, | ||
| optimizeCss: false, | ||
| gzipSize: false, | ||
| } | ||
| : { | ||
| optimizePackageImports: [ | ||
| 'lucide-react', | ||
| '@radix-ui/react-icons', | ||
| '@radix-ui/react-accordion', | ||
| '@radix-ui/react-alert-dialog', | ||
| '@radix-ui/react-avatar', | ||
| '@radix-ui/react-checkbox', | ||
| '@radix-ui/react-dialog', | ||
| '@radix-ui/react-dropdown-menu', | ||
| '@radix-ui/react-label', | ||
| '@radix-ui/react-popover', | ||
| '@radix-ui/react-progress', | ||
| '@radix-ui/react-scroll-area', | ||
| '@radix-ui/react-select', | ||
| '@radix-ui/react-separator', | ||
| '@radix-ui/react-slider', | ||
| '@radix-ui/react-slot', | ||
| '@radix-ui/react-switch', | ||
| '@radix-ui/react-tabs', | ||
| '@radix-ui/react-toast', | ||
| '@radix-ui/react-tooltip', | ||
| 'recharts', | ||
| 'date-fns', | ||
| 'react-virtuoso', | ||
| 'framer-motion', | ||
| ], | ||
| optimizeCss: true, // critters 기반 critical CSS 인라인화 | ||
| gzipSize: true, | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Next.js 16 experimental config options cpus memoryBasedWorkersCount webpackBuildWorker webpackMemoryOptimizations turbopackMemoryLimit
💡 Result:
In Next.js 16, several memory and build-related configuration options exist within the experimental namespace of next.config.js to manage resource utilization during development and production builds [1][2][3]. The following are the details for the requested experimental configuration options: webpackBuildWorker: A boolean flag used to run the Webpack build in a separate Node.js worker process [2][4][3]. This is designed to decrease memory usage during builds [4]. It defaults to true if the application does not have a custom Webpack configuration; otherwise, it is disabled by default [2][4][3]. webpackMemoryOptimizations: A boolean flag that enables optimizations to reduce the maximum heap size used by Webpack [2][3]. While this can lower memory consumption, it may slightly increase compilation times [2][4][3]. It defaults to false [2][3]. turbopackMemoryLimit: A numeric value representing the target memory limit for Turbopack, specified in bytes [2][3]. cpus: An experimental numeric configuration option [1][2]. In the Next.js internal configuration schema, this is defined as a number, intended to specify the number of CPUs available for build or processing tasks [1][2]. memoryBasedWorkersCount: A boolean flag [2]. When enabled, this experimental option allows the system to determine the number of workers based on available memory, rather than solely on CPU count [1][2]. Note that many features previously categorized under experimental in earlier versions of Next.js have been moved to stable or restructured in Next.js 16 (for example, the primary configuration for Turbopack moved to a top-level property outside of experimental) [5][6]. Users should verify if specific features have been stabilized or renamed by reviewing the latest official Next.js documentation [7][6].
Citations:
- 1: https://cdn.jsdelivr.net/npm/next@16.1.3/dist/server/config-schema.d.ts
- 2: https://cdn.jsdelivr.net/npm/next@16.1.3/dist/server/config-shared.d.ts
- 3: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/config-shared.ts
- 4: https://nextjs.org/docs/app/guides/memory-usage
- 5: https://nextjs.org/blog/next-16
- 6: https://nextjs.org/docs/app/guides/upgrading/version-16
- 7: https://nextjs.org/docs/app/api-reference/config/next-config-js
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ls -la
if [ -f "next.config.js" ]; then
echo "=== next.config.js (lines 1-160) ==="
nl -ba next.config.js | sed -n '1,160p'
else
echo "next.config.js not found in repo root"
fiRepository: jlinsights/ASCA
Length of output: 5876
🌐 Web query:
Next.js 16 experimental optimizeCss gzipSize
💡 Result:
In Next.js 16, both experimental.optimizeCss and experimental.gzipSize are legacy or deprecated configurations [1][2]. 1. experimental.optimizeCss: This feature was designed to inline critical CSS using the critters library, primarily for the Pages Router [3][4]. It is incompatible with the App Router because critters does not support the streaming architecture utilized by the App Router [3][5][4]. While the flag still exists in the codebase (as of v16.1.3), it is not recommended for modern Next.js applications and does not function as intended in App Router-based projects [1][3]. 2. experimental.gzipSize: As of Next.js 16, this configuration is considered a no-op [1][2]. Build-time size metrics were removed from the build output, rendering this option obsolete [1][2]. If you are looking to manage CSS delivery or optimize performance in a modern Next.js 16 application using the App Router, you should rely on standard optimizations rather than these experimental legacy flags [4][6].
Citations:
- 1: https://cdn.jsdelivr.net/npm/next@16.1.3/dist/server/config-shared.d.ts
- 2: https://github.com/vercel/next.js/blob/canary/packages/next/src/server/config-shared.ts
- 3: is experimental: { optimizeCss: true } still in use for nextJS v14.2 vercel/next.js#80486
- 4: https://www.corewebvitals.io/pagespeed/nextjs-remove-render-blocking-css
- 5: optimizeCss doesn't include any inline style vercel/next.js#53863
- 6: https://nextjs.org/docs/app/guides/upgrading/version-16
🌐 Web query:
Next.js 16 experimental optimizePackageImports optimizeCss gzipSize experimental namespace
💡 Result:
In Next.js 16, the experimental configuration landscape regarding CSS and bundle optimization has seen significant changes as the framework continues to evolve toward server-driven architectures. optimizePackageImports This feature remains available as an experimental option in next.config.js [1][2]. It is designed to reduce performance issues caused by large packages with many named exports by ensuring only the modules actually used in your application are loaded [1][2]. A broad range of libraries, such as lucide-react, date-fns, lodash-es, and various MUI and icon packages, are optimized by default [1]. optimizeCss The experimental optimizeCss flag is primarily intended for the Pages Router and uses the Critters library to inline critical CSS [3]. It is not compatible with the App Router because Critters requires full rendered HTML, which conflicts with streaming architectures [3][4]. In Next.js 16, users have reported that this feature can cause issues (e.g., context-related errors during prerendering), and it remains an experimental, non-stable feature [5][4]. gzipSize As of Next.js 16, the gzipSize configuration option is effectively a no-op [6]. Next.js 16 has removed size metrics, including First Load JS, from the build output, as these metrics were found to be inaccurate for server-driven architectures utilizing React Server Components [7]. Experimental Namespace The experimental namespace in next.config.js is still used for testing features that are not yet stabilized [1][2]. However, several previously experimental configurations have been moved or removed in Next.js 16. For example, the turbopack configuration was moved to the top-level next.config.js (no longer under experimental), and the experimental PPR (Partial Prerendering) flag was removed in favor of the new Cache Components model [8][7]. Users are advised to check the official Next.js 16 upgrading guide to see if specific experimental flags they were using have been stabilized, moved, or deprecated [7].
Citations:
- 1: https://nextjs.org/docs/pages/api-reference/config/next-config-js/optimizePackageImports
- 2: https://nextjs.org/docs/app/api-reference/config/next-config-js/optimizePackageImports
- 3: https://www.corewebvitals.io/pagespeed/nextjs-remove-render-blocking-css
- 4: optimizeCss doesn't include any inline style vercel/next.js#53863
- 5: Archit1706/temporary-road-closures@30d7b04
- 6: https://cdn.jsdelivr.net/npm/next@16.1.3/dist/server/config-shared.d.ts
- 7: https://github.com/vercel/next.js/blob/v16.2.1/docs/01-app/02-guides/upgrading/version-16.mdx
- 8: https://nextjs.org/blog/next-16
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Next.js version from package.json:"
node -p "require('./node_modules/next/package.json').version" 2>/dev/null || echo "next package.json not found"
echo
echo "next.config.js lines 65-130:"
sed -n '65,130p' next.config.js
echo
echo "next.config.js: experimental.lowMemoryBuild 관련 키 등장 여부:"
rg -n "lowMemoryBuild|cpus|memoryBasedWorkersCount|webpackBuildWorker|webpackMemoryOptimizations|turbopackMemoryLimit|optimizeCss|gzipSize|optimizePackageImports" next.config.jsRepository: jlinsights/ASCA
Length of output: 2380
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate Next's config schema/shared source inside node_modules
echo "Find config-shared.* under node_modules/next:"
find node_modules/next -maxdepth 6 -type f \( -name "config-shared.*" -o -name "*config-schema*" \) | head -n 50
echo
# Check whether the experimental keys are present in the shipped schema/types
echo "Search for experimental keys in Next shipped sources:"
rg -n "experimental\\.(cpus|memoryBasedWorkersCount|webpackBuildWorker|webpackMemoryOptimizations|turbopackMemoryLimit|optimizeCss|gzipSize|optimizePackageImports)" node_modules/next/dist -S || true
echo
echo "Search for 'turbopackMemoryLimit' anywhere in Next shipped sources:"
rg -n "turbopackMemoryLimit" node_modules/next/dist -S || trueRepository: jlinsights/ASCA
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_TS="node_modules/next/dist/server/config-shared.d.ts"
FILE_JS="node_modules/next/dist/server/config-shared.js"
FILE_SCHEMA="node_modules/next/dist/server/config-schema.js"
for f in "$FILE_TS" "$FILE_JS" "$FILE_SCHEMA"; do
echo "== $f =="
wc -l "$f"
done
echo
echo "== experimental.cpus etc in config-shared.d.ts =="
rg -n "experimental\\.(cpus|memoryBasedWorkersCount|webpackBuildWorker|webpackMemoryOptimizations|turbopackMemoryLimit|optimizeCss|gzipSize|optimizePackageImports)" node_modules/next/dist/server/config-shared.d.ts || true
echo
echo "== experimental keys near optimizeCss/gzipSize in config-shared.js =="
rg -n "optimizeCss|gzipSize|turbopackMemoryLimit|webpackMemoryOptimizations|webpackBuildWorker|memoryBasedWorkersCount|cpus" node_modules/next/dist/server/config-shared.js | head -n 50 || true
echo
echo "== experimental.optimizeCss/gzipSize usage in define-env.js (checks whether they affect build) =="
rg -n "__NEXT_OPTIMIZE_CSS|__NEXT_GZIP|gzipSize" node_modules/next/dist/build/define-env.js | head -n 80 || true
echo
echo "== config schema: is there validation for these keys? (cpus etc) =="
rg -n "(cpus|memoryBasedWorkersCount|webpackBuildWorker|webpackMemoryOptimizations|turbopackMemoryLimit|optimizeCss|gzipSize)" node_modules/next/dist/server/config-schema.js | head -n 80 || trueRepository: jlinsights/ASCA
Length of output: 1464
🌐 Web query:
Next.js 16.2 experimental cpus memoryBasedWorkersCount webpackBuildWorker webpackMemoryOptimizations turbopackMemoryLimit optimizeCss gzipSize
💡 Result:
In Next.js 16.2, several configuration options related to build performance and memory management have been updated, deprecated, or removed. cpus: This experimental configuration allows you to manually specify the number of CPUs used by Next.js for concurrent tasks. memoryBasedWorkersCount: This experimental setting, which previously allowed for dynamic worker scaling based on system memory, is generally considered legacy or non-functional in recent versions as Next.js has moved toward internal optimizations for worker management [1][2]. webpackBuildWorker: This feature is enabled by default in modern Next.js versions (starting from 14.1.0) for projects without custom Webpack configurations [3]. It runs Webpack compilations in a separate Node.js worker to reduce build-time memory usage. If you have a custom Webpack configuration, you can explicitly enable it by setting experimental.webpackBuildWorker: true in your next.config.js [3][4]. webpackMemoryOptimizations: Introduced in Next.js 15.0.0, this experimental flag (experimental.webpackMemoryOptimizations: true) alters Webpack behavior to reduce peak memory usage, though it may result in slightly longer compilation times [3][4]. It is not compatible with the Webpack build worker option [3][4]. turbopackMemoryLimit: This configuration option has been removed from Next.js as of June 2026, as it was determined to be non-functional [5]. optimizeCss: This legacy experimental option has been largely superseded by modern CSS handling in Next.js. gzipSize: This flag is considered deprecated or a no-op in Next.js 16. As of version 16, size metrics are no longer generated in the build output, rendering this configuration unnecessary [2].
Citations:
- 1: https://cdn.jsdelivr.net/npm/next@14.2.19/dist/server/config-shared.d.ts
- 2: https://cdn.jsdelivr.net/npm/next@16.1.3/dist/server/config-shared.d.ts
- 3: https://nextjs.org/docs/app/guides/memory-usage
- 4: https://nextjs.im/docs/app/guides/memory-usage/
- 5: Remove turbopackMemoryLimit it is dead vercel/next.js#94483
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Focused searches to see whether flags are actually used in Next 16.2.7 codepaths.
rg -n "gzipSize" node_modules/next/dist -S | head -n 80
rg -n "turbopackMemoryLimit" node_modules/next/dist -S | head -n 80
rg -n "experimental\\.cpus|cpus:" node_modules/next/dist -S | head -n 80
rg -n "memoryBasedWorkersCount|webpackBuildWorker|webpackMemoryOptimizations" node_modules/next/dist -S | head -n 120Repository: jlinsights/ASCA
Length of output: 50373
🌐 Web query:
Next.js 16.2.7 gzipSize no-op experimental.optimizeCss gzipSize
💡 Result:
In Next.js 16.2.7, the configuration options mentioned function as follows: Experimental optimizeCss: The experimental.optimizeCss: true flag is an older feature designed specifically for the Pages Router to inline critical CSS using the critters library [1][2]. It is not compatible with the App Router because critters does not support streaming, a core architectural requirement of the App Router [1][2][3]. Because it is experimental and has limited maintenance, it is not recommended for production use [2][3]. For users of the App Router looking to optimize CSS, newer experimental alternatives like inlineCss have been explored [2]. gzipSize: The gzipSize option is an internal utility flag rather than a general public configuration for production builds [4][5]. It is used primarily in build utilities to determine whether to calculate and report compressed (gzip) sizes versus uncompressed sizes during build time (e.g., when generating manifest data or bundle statistics) [4][6]. There is no "no-op" mode associated with it in the standard next.config.js schema; rather, it functions as a boolean parameter in internal build functions to toggle size reporting logic [4]. Summary of Key Points: - experimental.optimizeCss: Intended for Pages Router only; deprecated/unsupported in App Router [1][2]. - gzipSize: An internal build-time flag, not a user-facing configuration for runtime compression [4]. - Compression: Standard HTTP compression (gzip/brotli) is handled by Next.js by default via the compress option, or by the hosting platform (e.g., Vercel's Edge Network) [7].
Citations:
- 1: is experimental: { optimizeCss: true } still in use for nextJS v14.2 vercel/next.js#80486
- 2: https://www.corewebvitals.io/pagespeed/nextjs-remove-render-blocking-css
- 3: optimizeCss doesn't include any inline style vercel/next.js#53863
- 4: https://github.com/vercel/next.js/blob/5b9ad8da909a7870280c39340c189392d6307994/packages/next/build/utils.ts
- 5: https://newreleases.io/project/github/vercel/next.js/release/v10.2.1-canary.7
- 6: [Turbopack] write route bundle stats to .next/diagnostics/route-bundle-stats.json vercel/next.js#90949
- 7: https://nextjs.im/docs/14/app/api-reference/next-config-js/compress/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# gzipSize 실제 사용처 파악(설정 스키마/타입 제외)
rg -n "gzipSize" node_modules/next/dist -S --glob '!**/*.d.ts' --glob '!**/*.map' | head -n 80
echo "----"
# optimizeCss 실제 사용처 파악
rg -n "optimizeCss" node_modules/next/dist -S --glob '!**/*.d.ts' --glob '!**/*.map' | head -n 80
echo "----"
# App Router에서 optimizeCss가 우회/제약되는지(critters/inline critical 관련)
rg -n "critters|critical CSS|inline critical" node_modules/next/dist -S --glob '!**/*.d.ts' --glob '!**/*.map' | head -n 80
echo "----"
# turbopackMemoryLimit 실제 사용 여부
rg -n "turbopackMemoryLimit" node_modules/next/dist -S --glob '!**/*.d.ts' --glob '!**/*.map' | head -n 80
echo "----"
# low memory 옵션들이 어디에서 참조되는지: cpus / memoryBasedWorkersCount / webpackBuildWorker 관련
rg -n "memoryBasedWorkersCount|webpackBuildWorker|webpackMemoryOptimizations|experimental\\.cpus|config\\.experimental\\.cpus|__NEXT_.*WORKER" node_modules/next/dist -S --glob '!**/*.d.ts' --glob '!**/*.map' | head -n 120Repository: jlinsights/ASCA
Length of output: 50373
🌐 Web query:
Next.js 16 optimizeCss App Router critters __NEXT_OPTIMIZE_CSS behavior
💡 Result:
In Next.js 16, the experimental.optimizeCss configuration—which historically relied on the critters library to inline critical CSS—is not supported for use with the App Router [1][2][3]. Key details regarding this behavior include: Architecture Incompatibility: The optimizeCss feature (and the underlying critters/beasties post-processing) requires access to the full, rendered HTML document to identify and inline critical styles [2][3]. Because the App Router utilizes React Server Components and streaming architecture, it does not provide a complete static HTML string at the time of processing, making it fundamentally incompatible with this specific optimization approach [2][3]. Library Status: The critters library, which previously powered this feature, reached end-of-life and was officially replaced by the actively maintained fork, Beasties, within the Next.js codebase (as of January 2026) to maintain support for legacy Pages Router projects [4][5]. While the underlying technology has been updated, this change does not extend optimizeCss functionality to the App Router [5]. Recommended Alternatives for App Router: Because optimizeCss does not function in the App Router, developers seeking to eliminate render-blocking CSS must use other strategies: - Inline CSS: Some versions and configurations of Next.js have experimented with experimental.inlineCss, which replaces standard stylesheet tags with inline style tags [2]. This approach differs from critical CSS extraction as it generally inlines the entire CSS bundle, which may impact payload size [6][2]. - Turbopack Optimization: Recent developments in Next.js 16 leverage advanced Turbopack capabilities, such as import attributes, to achieve more granular control over CSS loading by bypassing standard bundling pipelines for specific imports [6]. - Manual Optimization: Developers often rely on the native CSS-in-JS support or standard CSS module behavior while monitoring bundle sizes and applying manual preloading or strategic CSS management as needed [2][7]. In summary, do not enable optimizeCss for App Router projects as it will not perform critical CSS inlining and is not intended for that architectural model [1][2][3].
Citations:
- 1: optimizeCss doesn't include any inline style vercel/next.js#53863
- 2: https://www.corewebvitals.io/pagespeed/nextjs-remove-render-blocking-css
- 3: is experimental: { optimizeCss: true } still in use for nextJS v14.2 vercel/next.js#80486
- 4: Replace critters with beasties vercel/next.js#72036
- 5: chore(build): replace deprecated critters package with beasties in post-process vercel/next.js#88640
- 6: https://www.oleksiimazurenko.link/nb/blog/eliminating-render-blocking-css
- 7: App Router includes unused CSS module in unrelated routes (bundle analyzer + runtime) vercel/next.js#89252
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# gzipSize를 config.experimental.gzipSize로 읽는지/사용하는지 확인
rg -n "experimental\\.gzipSize|config\\.experimental\\.gzipSize|__NEXT_.*GZIP|gzipSize\\s*:" node_modules/next/dist -S --glob '!**/*.d.ts' --glob '!**/*.map' | head -n 80
echo "----"
# optimizeCss 사용처: __NEXT_OPTIMIZE_CSS 말고도 critters/inline critical 관련 실제 코드가 있는지
rg -n "experimental\\.optimizeCss|config\\.experimental\\.optimizeCss|__NEXT_OPTIMIZE_CSS|critters|beasties|inline critical" node_modules/next/dist -S --glob '!**/*.d.ts' --glob '!**/*.map' | head -n 120
echo "----"
# worker 관련 옵션 실제 사용(예: webpack/turbopack worker 스케줄러에서 참조) 확인
rg -n "experimental\\.(cpus|memoryBasedWorkersCount|webpackBuildWorker|webpackMemoryOptimizations|turbopackMemoryLimit)" node_modules/next/dist -S --glob '!**/*.d.ts' --glob '!**/*.map' | head -n 120
echo "----"
# config-shared.js에서 optimizeCss/gzipSize default 처리와 define-env 연결 여부 확인
rg -n "__NEXT_OPTIMIZE_CSS|experimental\\.optimizeCss|gzipSize" node_modules/next/dist/build/define-env.js -S --glob '!**/*.d.ts' --glob '!**/*.map' | head -n 80Repository: jlinsights/ASCA
Length of output: 50372
Next.js 16 experimental.lowMemoryBuild 옵션의 “유효성”과 “번들 크기 트레이드오프” 가정 보정
- 현재 프로젝트의 Next.js 버전은 16.2.7이며,
experimental.cpus,memoryBasedWorkersCount,webpackBuildWorker,webpackMemoryOptimizations,turbopackMemoryLimit,optimizeCss,gzipSize는 Next 16.2.7의experimental스키마에 허용되는 키로 존재합니다. 따라서 “유효성 자체(스키마/타입)” 이슈로 보는 우선순위는 낮습니다. - 다만
experimental.optimizeCss는 **Pages Router용(critters 기반)**으로 알려져 있어 App Router에서는 의도대로 동작하지 않는 항목입니다. 따라서lowMemoryBuild에서optimizeCss: false가 “번들 크기를 증가”시킨다는 표현은 근거가 약합니다. experimental.gzipSize는 구성/스키마상 존재하지만,false가 실제 산출물 번들 크기를 늘린다는 근거는 보이지 않습니다(HTTP gzip 전송은compress/호스팅에서 처리). “gzipSize trade-off로 번들 증가” 가정은 실측 비교(빌드 산출물/zip 크기, 네트워크 전송 크기)로 재확인이 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@next.config.js` around lines 72 - 111, The review incorrectly asserts that
experimental.optimizeCss:false and gzipSize:false in the lowMemoryBuild branch
increase bundle size; update the config and comments to reflect Next.js 16.2.7
behavior: keep the lowMemoryBuild object as-is but remove or correct any
comment/assumption that “optimizeCss: false” increases bundle size (note that
optimizeCss is Pages Router / critters-specific) and that gzipSize toggles
on-disk bundle size (explain gzipSize is for reporting, actual transport gzip is
handled by hosting), and add a TODO to run measured build size comparisons if
the trade-off needs validation; reference the experimental.lowMemoryBuild branch
and keys experimental.optimizeCss and experimental.gzipSize when making these
changes.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/members/route.ts`:
- Around line 130-173: The E2E branch short-circuits the request pipeline
(isE2ETest block) before applying protections—move or mirror the protection
checks so API key validation, admin authorization, and rate limiting still run
for E2E flows; specifically ensure readLimiter/writeLimiter and the API
key/admin checks are executed before returning e2eMembersResponse, or explicitly
call those same checks inside the isE2ETest branch around getE2EMembers and
e2eMembersResponse; apply the same fix to the analogous E2E handling in the
POST/mutation branch (around lines handling create/update code referenced in the
diff, e.g., where writeLimiter would be used).
In `@proxy.ts`:
- Around line 8-9: The current IS_E2E_TEST calculation (const IS_E2E_TEST =
process.env.NODE_ENV === 'test' || Boolean(process.env.PLAYWRIGHT_BASE_URL)) is
unsafe because the mere presence of PLAYWRIGHT_BASE_URL can flip the app into
E2E mode and cause proxy to pick e2eProxy and bypass auth.protect(); change the
condition to require a strict, explicit signal (e.g., NODE_ENV === 'test' OR
PLAYWRIGHT_BASE_URL is present AND an explicit flag like PLAYWRIGHT_E2E_ENABLED
=== 'true' or PLAYWRIGHT_MODE === 'e2e') so accidental env leakage cannot enable
E2E mode, update the IS_E2E_TEST constant accordingly and ensure the proxy
selection logic that references IS_E2E_TEST and e2eProxy continues to use the
tightened check.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 96df1bf3-dec2-47be-9b35-6786ccda674e
📒 Files selected for processing (5)
.github/workflows/ci.yml.github/workflows/e2e-tests.ymlapp/api/members/route.tse2e/api/members.spec.tsproxy.ts
| if (isE2ETest) { | ||
| const { searchParams } = new URL(request.url) | ||
| const pageParam = Number(searchParams.get('page')) | ||
| const limitParam = Number(searchParams.get('limit')) | ||
| const parsed = { | ||
| query: searchParams.get('query') || undefined, | ||
| page: Number.isInteger(pageParam) && pageParam > 0 ? pageParam : 1, | ||
| limit: Number.isInteger(limitParam) && limitParam > 0 ? Math.min(limitParam, 100) : 20, | ||
| status: memberStatusSchema.safeParse(searchParams.get('status')).success | ||
| ? (searchParams.get('status') as E2EMember['membership_status']) | ||
| : undefined, | ||
| level: searchParams.get('level') || undefined, | ||
| sortOrder: searchParams.get('sortOrder') === 'asc' ? 'asc' : 'desc', | ||
| } | ||
|
|
||
| let membersList = getE2EMembers() | ||
| if (parsed.query) { | ||
| const query = parsed.query.toLowerCase() | ||
| membersList = membersList.filter(member => | ||
| [ | ||
| member.email, | ||
| member.first_name_ko, | ||
| member.last_name_ko, | ||
| member.first_name_en, | ||
| member.last_name_en, | ||
| ] | ||
| .filter(Boolean) | ||
| .some(value => value!.toLowerCase().includes(query)) | ||
| ) | ||
| } | ||
| if (parsed.status) { | ||
| membersList = membersList.filter(member => member.membership_status === parsed.status) | ||
| } | ||
| if (parsed.level) { | ||
| membersList = membersList.filter(member => member.membership_level_id === parsed.level) | ||
| } | ||
| membersList = membersList.sort((a, b) => { | ||
| const left = Date.parse(a.created_at) | ||
| const right = Date.parse(b.created_at) | ||
| return parsed.sortOrder === 'asc' ? left - right : right - left | ||
| }) | ||
|
|
||
| return e2eMembersResponse(membersList, parsed.page, parsed.limit) | ||
| } |
There was a problem hiding this comment.
E2E 분기에서 API 보호 체인이 조기 우회됩니다.
현재 E2E 분기가 readLimiter/writeLimiter보다 먼저 반환되어 요청 보호 체인이 비활성화됩니다. 특히 POST(데이터 변이)는 E2E 분기에서 최소 필드만 검사하고 생성을 허용해, 테스트 플래그 오설정 시 방어면이 크게 낮아집니다. E2E 경로도 최소한 API key/권한/레이트리밋의 핵심 체크는 유지하는 편이 안전합니다.
As per coding guidelines, "Validate API key in request headers for all API routes", "Enforce admin authorization on privileged endpoints (e.g., migration, sync, data mutations)", and "Apply request rate limiting to protect API endpoints".
Also applies to: 260-290
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/members/route.ts` around lines 130 - 173, The E2E branch
short-circuits the request pipeline (isE2ETest block) before applying
protections—move or mirror the protection checks so API key validation, admin
authorization, and rate limiting still run for E2E flows; specifically ensure
readLimiter/writeLimiter and the API key/admin checks are executed before
returning e2eMembersResponse, or explicitly call those same checks inside the
isE2ETest branch around getE2EMembers and e2eMembersResponse; apply the same fix
to the analogous E2E handling in the POST/mutation branch (around lines handling
create/update code referenced in the diff, e.g., where writeLimiter would be
used).
Source: Coding guidelines
| const IS_E2E_TEST = process.env.NODE_ENV === 'test' || Boolean(process.env.PLAYWRIGHT_BASE_URL) | ||
|
|
There was a problem hiding this comment.
E2E 분기 조건이 운영 인증 우회로 이어질 수 있습니다.
PLAYWRIGHT_BASE_URL 존재만으로 E2E 분기가 활성화되고, 그 즉시 proxy가 e2eProxy를 선택해 보호 라우트의 auth.protect()가 전면 비활성화됩니다. 배포 환경 변수 오설정 한 번으로 인증 우회가 발생할 수 있습니다.
🔧 제안 수정
-const IS_E2E_TEST = process.env.NODE_ENV === 'test' || Boolean(process.env.PLAYWRIGHT_BASE_URL)
+const IS_E2E_TEST =
+ process.env.NODE_ENV === 'test' &&
+ process.env.E2E_AUTH_BYPASS === 'true'
-export const proxy = IS_E2E_TEST ? e2eProxy : clerkProxy
+export const proxy = IS_E2E_TEST ? e2eProxy : clerkProxyAlso applies to: 59-59
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@proxy.ts` around lines 8 - 9, The current IS_E2E_TEST calculation (const
IS_E2E_TEST = process.env.NODE_ENV === 'test' ||
Boolean(process.env.PLAYWRIGHT_BASE_URL)) is unsafe because the mere presence of
PLAYWRIGHT_BASE_URL can flip the app into E2E mode and cause proxy to pick
e2eProxy and bypass auth.protect(); change the condition to require a strict,
explicit signal (e.g., NODE_ENV === 'test' OR PLAYWRIGHT_BASE_URL is present AND
an explicit flag like PLAYWRIGHT_E2E_ENABLED === 'true' or PLAYWRIGHT_MODE ===
'e2e') so accidental env leakage cannot enable E2E mode, update the IS_E2E_TEST
constant accordingly and ensure the proxy selection logic that references
IS_E2E_TEST and e2eProxy continues to use the tightened check.
Summary
proxy.tsfor Next.js 16 compatibility.GalleryPageClientinto lighter orchestration plusGalleryPagePartssubcomponents.Verification
npm run type-checkpassed locally.npm run lintpassed locally; existing max-lines warnings remain reduced from 11 to 10.npm run buildremains blocked by container memory limits/OOM; remote CI is the intended verification path.Notes
Summary by CodeRabbit
Refactor
Chores
Tests