fix(realtime): logic 12 fail 일괄 fix → 116/116 PASS (rev β #12)#22
fix(realtime): logic 12 fail 일괄 fix → 116/116 PASS (rev β #12)#22jlinsights wants to merge 5 commits into
Conversation
부모 사이클 tests-realtime-async-fix ejection 정식 처리. 3 sub-cause 식별 → source 2 file + test 2 file fix. Root Cause 1 (sse-manager production bug): - options.maxClients || 1000 → 0이 falsy → default 1000 됨 - Fix: || → ?? (nullish coalescing, 0 valid) Root Cause 2 (event-emitter wrapper): - on()이 wrappedListener 등록하지만 off()는 원본 listener 사용 - → EventEmitter에서 못 찾음 → 리스너 잔존 - Fix: listenerWrappers Map으로 원본 → wrapped 추적 Root Cause 3 (test singleton mismatch): - 프로덕션 매니저: getEventEmitter() (singleton) - 테스트: createEventEmitter() (new instance) → disconnected - Fix: 테스트도 getEventEmitter() 사용 Files (4): - lib/realtime/sse-manager.ts (3 lines): || → ?? - lib/realtime/event-emitter.ts (3 hunks): Map field + on/off update - lib/realtime/__tests__/websocket-manager.test.ts (4 occurrences) - lib/realtime/__tests__/e2e-flow.test.ts (12 occurrences) mini-do 검증: realtime 116/116 PASS, 0 fail (was 12 fail) CI 예상: 439 → 451 (+12) 15 chain 누적 예상: 228 → 451 (+223, +98%)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (14)
Note
|
| Layer / File(s) | Summary |
|---|---|
Design tokens (문서) DESIGN.md |
Framer 브랜드 디자인 시스템 정의: 색상·타이포그래피·간격·반경·컴포넌트 토큰 및 가이드 추가. |
CSS 토큰 및 유틸리티 app/globals.css, tailwind.config.ts |
:root CSS 커스텀 프로퍼티(--framer-*) 추가, 프레이머 타이포/버튼/카드/레이아웃 유틸리티 및 애니메이션 클래스 추가; 이전 광범위한 레거시 CSS 블록 제거. |
루트 레이아웃 색상 적용 app/layout.tsx |
<body> 배경/글자색을 Tailwind 클래스에서 var(--framer-canvas)/var(--framer-ink) 인라인 스타일로 변경; <html>에 className='dark'. |
헤더 리팩터 및 인증 UI components/header/header.tsx, components/header/header-auth-section.tsx |
헤더의 드롭다운/모바일 메뉴 스타일을 클래스 기반에서 인라인 스타일로 전환, Escape 핸들러 추가; 인증 로더 및 비로그인 버튼을 Framer 스타일로 변경(버튼 텍스트 일부 변경 포함). |
페이지/섹션 리팩터 components/sections/hero-section.tsx, components/sections/featured-exhibitions-section.tsx, components/layout/layout-footer.tsx, app/gallery/page.tsx, components/gallery/* |
Hero/FeaturedExhibitions/Gallery/Footer 등 다수 컴포넌트에서 Framer 스타일·인라인 스타일로 재구성(애니메이션 제거 또는 간소화, 카드/CTA/그리드·라이트박스·통계 등 재구현). |
프론트엔드 트윅(사용자 추적/JSX 변경) components/gallery/GalleryClient.tsx, 기타 |
gtag 페이로드 축소, 일부 불필요한 import 제거, 레이아웃/스타일링을 Framer 변수 기반으로 전환. |
실시간 통신 로직 수정
| Layer / File(s) | Summary |
|---|---|
이벤트 리스너 매핑 lib/realtime/event-emitter.ts |
AppEventEmitter에 listenerWrappers: Map 추가. on()이 원본→래퍼 매핑을 기록하고 off()는 매핑을 사용해 실제 등록된(래핑된) 리스너를 제거하도록 변경. |
SSE 옵션 기본값 처리 lib/realtime/sse-manager.ts |
keepAliveInterval, clientTimeout, maxClients의 기본값 처리에서 ` |
테스트 인스턴스 통일 lib/realtime/__tests__/e2e-flow.test.ts, lib/realtime/__tests__/websocket-manager.test.ts |
테스트들이 createEventEmitter() 대신 getEventEmitter()를 사용하도록 변경하여 싱글톤 이벤트 에미터 사용으로 통일. |
계획/문서 업데이트 docs/01-plan/features/tests-realtime-logic-fixes.plan.md, docs/01-plan/features/tests-stale-member-thenable-fix.plan.md |
realtime 관련 루트 원인 및 수정 패턴 문서 추가/갱신(thenable 처리 패턴 포함). |
레거시 계획 문서 제거 docs/01-plan/features/tests-*.plan.md (여러 파일) |
기존의 일부 테스트 계획 문서(복수)를 삭제. |
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
- fix(test): G1 doNotFake (53/80 timeout↓) + G2 verifyMember dead-delete #9: realtime 테스트(e2e/websocket-manager) 변경 내용과 테스트용 이벤트 에미터 사용 방식 변경에서 공통점이 있음.
🚥 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 제목이 변경 사항의 주요 내용(realtime 로직 12개 실패 수정 및 116/116 PASS)을 명확하게 요약하고 있습니다. |
| 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/tests-realtime-logic-fixes
Tip
💬 Introducing Slack Agent: The best way for teams to turn conversations into code.
Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
- Generate code and open pull requests
- Plan features and break down work
- Investigate incidents and troubleshoot customer tickets together
- Automate recurring tasks and respond to alerts with triggers
- Summarize progress and report instantly
Built for teams:
- Shared memory across your entire org—no repeating context
- Per-thread sandboxes to safely plan and execute work
- Governance built-in—scoped access, auditability, and budget controls
One agent for your entire SDLC. Right inside Slack.
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.
- Add Framer design tokens to globals.css (colors, typography, buttons, cards) - Register framer-* color tokens in tailwind.config.ts - Force dark canvas (#090909) in app/layout.tsx - Rewrite header with Framer top-nav: 56px height, pill CTAs, scroll blur - Rewrite hero section with display-xxl headline, extreme negative tracking - Rewrite exhibitions section with surface-1 cards + gradient spotlight cards - Rewrite footer with canvas background, caption typography, hairline dividers - Update header-auth-section with Framer primary/secondary pill buttons Implements DESIGN.md (version: alpha, name: Framer) Pure black canvas, white pill CTAs, violet/magenta/orange spotlight cards
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/globals.css`:
- Around line 71-73: Remove the duplicate CSS `@import` for Inter from globals.css
(the line importing 'https://fonts.googleapis.com/...Inter...') because Inter is
already loaded via next/font/google in app/layout.tsx; delete that `@import` so
you no longer violate the stylelint no-invalid-position-at-import-rule (imports
must come before other rules like Tailwind directives) and rely on the font
instance created in app/layout.tsx instead.
- Around line 75-102: The letter-spacing clamp() arguments in the
.framer-display-xxl, .framer-display-xl, and .framer-display-lg rules are
inverted (min/max swapped) and use a percent for the preferred value, which
prevents responsive behavior; update each letter-spacing to use clamp(min,
preferred, max) with the preferred value expressed in vw (e.g., use a negative
vw value between the min and max) so the spacing scales responsively across
breakpoints; adjust the three selectors (.framer-display-xxl,
.framer-display-xl, .framer-display-lg) accordingly ensuring the smallest (most
negative) px is the first argument, a vw-based middle argument, and the largest
(least negative) px is the third argument.
In `@components/header/header-auth-section.tsx`:
- Around line 37-45: The inline style on the SignInButton and SignUpButton
overrides the .btn-framer-* default touch target by setting minHeight: '36px';
restore accessibility by removing that override or changing minHeight to '44px'
(or use height: '44px') on the elements inside SignInButton and SignUpButton so
the buttons using classNames 'btn-framer-secondary' and 'btn-framer-primary'
have a 44px touch target.
In `@components/header/header.tsx`:
- Around line 230-232: The desktop dropdown currently only opens on hover
(onMouseEnter/onMouseLeave using handleMouseEnter(menu.key) and
handleMouseLeave) which blocks keyboard users; add corresponding onFocus and
onBlur (or onFocusCapture/onBlurCapture if needed) handlers to the same
element(s) that have menu.subItems to call handleMouseEnter(menu.key) and
handleMouseLeave so focus opens the menu and blur closes it, and update the
visibility logic that applies the "invisible" class (the block around lines
270-278) to consider the same open state used by hover (e.g., the menu's
isOpen/active state) so the submenu is not hidden when focused via keyboard.
- Around line 430-435: The mobile submenu is clipped because the open state uses
a fixed maxHeight ('400px'); change the style logic used where
mobileDropdowns[menu.key] is read so the open state uses a viewport-relative max
(e.g., '80vh' or 'calc(100vh - <headerHeight>px)') and enable internal scrolling
(overflowY: 'auto') while keeping overflow: 'hidden' for the closed state;
update the style object around mobileDropdowns[menu.key] to set maxHeight:
mobileDropdowns[menu.key] ? '80vh' : '0' (or calc with your header/footer
offset) and add overflowY: mobileDropdowns[menu.key] ? 'auto' : 'hidden' to
ensure tall menus can scroll instead of being cut off.
In `@components/layout/layout-footer.tsx`:
- Around line 126-132: The footer's fixed 6-column grid (gridTemplateColumns:
'1.5fr repeat(5, 1fr)') in the LayoutFooter component causes columns to compress
and horizontal overflow on small screens; update the style on the same div to
use a responsive grid such as auto-fit/minmax (e.g., 'gridTemplateColumns:
"repeat(auto-fit, minmax(160px, 1fr))"' or switch to breakpoint-specific
templates) so columns wrap naturally on narrow viewports and prevent overflow.
Ensure the change is applied where the inline style object is defined in
layout-footer.tsx so mobile behaves correctly.
In `@components/sections/hero-section.tsx`:
- Around line 61-185: The hero component bypasses the localization layer by
hardcoding all strings; use the existing useLanguage() hook (or app's i18n
accessor) to replace hardcoded text in the h1, span with gradient, <p> subhead,
Link CTAs, the stats array values/labels, and the "Scroll" span with locale keys
(e.g., hero.title, hero.subtitle, hero.cta.exhibitions, hero.cta.artworks,
hero.stats.[n].value/label, hero.scroll) so text is fetched via the hook (e.g.,
getString/t from useLanguage()) instead of literals; update the stats mapping to
read from the localization object and keep component structure (h1, p, Link,
stats array, Scroll span) intact.
In `@DESIGN.md`:
- Around line 1-4: The document is triggering MD041/MD022 because there is no
top-level H1 after the frontmatter and several subheadings (notably "### Brand &
Accent" and the headings under it) lack the required blank lines; add a single
H1 title line immediately after the YAML frontmatter (e.g., "# Framer") and
ensure there is a blank line before and after each subheading under "### Brand &
Accent" (and the same fixes for the block around lines 256-296) so the linter no
longer reports MD041/MD022.
In `@docs/01-plan/features/tests-realtime-logic-fixes.plan.md`:
- Around line 28-53: Add blank lines before and after each heading and fenced
code block, and escape the pipe characters in the table header row by replacing
"||" with "\|\|" in the markdown line that shows the bug example (specifically
the snippet "maxClients: options.maxClients || 1000"); ensure the code fence
blocks (```ts) have an empty line above and below and that the table row using
"||" is changed to use "\|\|" so markdownlint no longer reports missing
surrounding blank lines or a malformed table column count.
In `@lib/realtime/__tests__/e2e-flow.test.ts`:
- Around line 16-17: The shared getEventEmitter() singleton is not being reset
between tests, causing subscriptions/listeners to leak; add an afterEach hook
that fetches the singleton via getEventEmitter() and calls its shutdown() or
reset method (or removeAllListeners()/clearSubscriptions() if shutdown is not
available) to tear down subscriptions created during each test and ensure test
isolation for tests that use createSubscriptionManager and
EventType/EventPayload.
In `@lib/realtime/event-emitter.ts`:
- Line 111: listenerWrappers currently maps only by the original EventListener,
so registering the same callback for different event types/filters overwrites
previous wrappers and off() removes the wrong subscription; change the tracking
to include the event identity (e.g., use a composite key of type+listener+filter
or a nested Map keyed by event type then listener then filter) and update the
add/on, off, and removeAllListeners implementations (referencing
listenerWrappers, off(), removeAllListeners(), and the addListener/on code
around lines ~240-296) to look up and remove the exact wrapper for the specific
(type, listener, filter) combination, and ensure removeAllListeners() clears the
listenerWrappers map for the removed scope.
In `@lib/realtime/sse-manager.ts`:
- Around line 80-82: The change to use nullish coalescing for keepAliveInterval
and clientTimeout is unsafe because clients can pass 0; restore safe behavior by
not treating 0 as a valid override for these timers (unlike maxClients).
Specifically, update the assignment logic for keepAliveInterval and
clientTimeout (symbols: keepAliveInterval, clientTimeout, options) to either
keep the original falsy-default behavior or explicitly validate that provided
values are positive numbers (e.g., use options.keepAliveInterval only if it is a
number > 0, otherwise use 30000; same for clientTimeout with 300000); leave
maxClients using ?? so 0 remains allowed.
🪄 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: 2be7f0e1-5bc4-4fce-aa34-3af8ebc05ca8
📒 Files selected for processing (21)
.commit_message.txtDESIGN.mdapp/globals.cssapp/layout.tsxcomponents/header/header-auth-section.tsxcomponents/header/header.tsxcomponents/layout/layout-footer.tsxcomponents/sections/featured-exhibitions-section.tsxcomponents/sections/hero-section.tsxdocs/01-plan/features/tests-realtime-async-fix.plan.mddocs/01-plan/features/tests-realtime-logic-fixes.plan.mddocs/01-plan/features/tests-route-error-policy.plan.mddocs/01-plan/features/tests-route-mutation-auth.plan.mddocs/01-plan/features/tests-stale-member-extras-spike.plan.mddocs/01-plan/features/tests-stale-member-schema-rewrite.plan.mddocs/01-plan/features/tests-stale-member-thenable-fix.plan.mdlib/realtime/__tests__/e2e-flow.test.tslib/realtime/__tests__/websocket-manager.test.tslib/realtime/event-emitter.tslib/realtime/sse-manager.tstailwind.config.ts
💤 Files with no reviewable changes (6)
- docs/01-plan/features/tests-route-mutation-auth.plan.md
- docs/01-plan/features/tests-route-error-policy.plan.md
- docs/01-plan/features/tests-stale-member-extras-spike.plan.md
- docs/01-plan/features/tests-realtime-async-fix.plan.md
- docs/01-plan/features/tests-stale-member-thenable-fix.plan.md
- docs/01-plan/features/tests-stale-member-schema-rewrite.plan.md
| /* ── Framer Typography Utilities ─────────────────────────── */ | ||
| /* GT Walsheim 미보유 시 Mona Sans / Inter 700 대체 */ | ||
| @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "app/layout.tsx" -o -name "app/globals.css" | head -20Repository: jlinsights/ASCA
Length of output: 41
🏁 Script executed:
cat -n app/layout.tsx | head -50Repository: jlinsights/ASCA
Length of output: 1795
🏁 Script executed:
cat -n app/globals.css | sed -n '65,110p'Repository: jlinsights/ASCA
Length of output: 1769
🏁 Script executed:
rg "letter-spacing.*clamp" app/globals.css -nRepository: jlinsights/ASCA
Length of output: 206
🏁 Script executed:
cat -n app/globals.css | sed -n '1,75p'Repository: jlinsights/ASCA
Length of output: 2629
중복된 @import 제거하고 next/font/google만 사용하세요.
Line 73의 @import는 이미 app/layout.tsx에서 next/font/google으로 로드한 Inter를 다시 요청합니다. 또한 Tailwind 지시어 이후의 위치에 있어 stylelint no-invalid-position-at-import-rule 규칙을 위반합니다. CSS @import는 다른 규칙들보다 반드시 앞에 위치해야 하므로, 이 라인을 제거하는 것이 올바른 방법입니다.
제안 diff
-@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');📝 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.
| /* ── Framer Typography Utilities ─────────────────────────── */ | |
| /* GT Walsheim 미보유 시 Mona Sans / Inter 700 대체 */ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); | |
| /* ── Framer Typography Utilities ─────────────────────────── */ | |
| /* GT Walsheim 미보유 시 Mona Sans / Inter 700 대체 */ |
🧰 Tools
🪛 Stylelint (17.9.0)
[error] 73-73: Expected "url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap')" to be "'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'" (import-notation)
(import-notation)
[error] 73-73: Invalid position for @import rule (no-invalid-position-at-import-rule)
(no-invalid-position-at-import-rule)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/globals.css` around lines 71 - 73, Remove the duplicate CSS `@import` for
Inter from globals.css (the line importing
'https://fonts.googleapis.com/...Inter...') because Inter is already loaded via
next/font/google in app/layout.tsx; delete that `@import` so you no longer violate
the stylelint no-invalid-position-at-import-rule (imports must come before other
rules like Tailwind directives) and rely on the font instance created in
app/layout.tsx instead.
| .framer-display-xxl { | ||
| font-family: 'Mona Sans', 'Inter', sans-serif; | ||
| font-size: clamp(52px, 8vw, 110px); | ||
| font-weight: 700; | ||
| line-height: 0.85; | ||
| letter-spacing: clamp(-2.6px, -5%, -5.5px); | ||
| font-feature-settings: 'ss02' on; | ||
| color: var(--framer-ink); | ||
| } | ||
|
|
||
| .framer-display-xl { | ||
| font-family: 'Mona Sans', 'Inter', sans-serif; | ||
| font-size: clamp(40px, 6vw, 85px); | ||
| font-weight: 700; | ||
| line-height: 0.95; | ||
| letter-spacing: clamp(-2px, -5%, -4.25px); | ||
| font-feature-settings: 'ss02' on; | ||
| color: var(--framer-ink); | ||
| } | ||
|
|
||
| .framer-display-lg { | ||
| font-family: 'Mona Sans', 'Inter', sans-serif; | ||
| font-size: clamp(32px, 4.5vw, 62px); | ||
| font-weight: 700; | ||
| line-height: 1.00; | ||
| letter-spacing: clamp(-1.6px, -5%, -3.1px); | ||
| font-feature-settings: 'ss02' on; | ||
| color: var(--framer-ink); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the actual file and its content at the specified lines
fd -t f "globals.css" app/ | head -20Repository: jlinsights/ASCA
Length of output: 75
🌐 Web query:
letter-spacing CSS property accepted values units percentage
💡 Result:
The CSS letter-spacing property accepts two types of values: the keyword 'normal' or a . 'normal' uses the font's default spacing, allowing user agents to adjust for justification. adds extra space (positive or negative) to the default: - : Any CSS length unit (e.g., 3px, 0.3em, -0.5px). - : Relative to the width of the space character (U+0020) glyph in the font (e.g., 50%, 12%). Syntax: letter-spacing: normal | ; Examples: letter-spacing: normal; letter-spacing: 1px; letter-spacing: 0.1em; letter-spacing: 30%;
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/CSS/letter-spacing
- 2: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/letter-spacing
🏁 Script executed:
cat -n app/globals.css | sed -n '75,102p'Repository: jlinsights/ASCA
Length of output: 1038
🌐 Web query:
CSS clamp() function validation MIN MAX order invalid declaration
💡 Result:
In CSS, the clamp function does not strictly validate that the minimum value is less than or equal to the maximum value at parse time; instead, it computes the result as max(MIN, min(VAL, MAX)), which handles cases where MIN > MAX gracefully by effectively swapping their roles (the larger argument becomes the effective max, the smaller the effective min). This is not considered an "invalid declaration" by browsers, and the declaration remains valid. However, it leads to unexpected behavior and is strongly discouraged—always ensure MIN ≤ MAX for predictable clamping. Official MDN documentation confirms clamp(MIN, VAL, MAX) resolves to max(MIN, min(VAL, MAX)) without mentioning syntax invalidation for order. Secondary sources note that reversed order "might not work as you'd expect" or causes confusion, but the property applies using the computed bounds.
Citations:
- 1: https://developer.mozilla.org/en-US/docs/web/css/clamp
- 2: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/clamp
- 3: https://runebook.dev/en/docs/css/clamp
- 4: https://blog.logrocket.com/min-max-clamp-css-functions/
display 타이포의 letter-spacing clamp() 인자 순서가 잘못되었습니다.
clamp(-2.6px, -5%, -5.5px)는 최솟값이 최댓값보다 크므로 의도한 반응형 스케일링이 작동하지 않습니다. 브라우저는 선언을 무효 처리하지 않지만, 계산 결과가 항상 -2.6px로 고정되어 중간값의 가변성이 무시됩니다. 반응형 자간 조정이 필요하면 인자 순서를 바르게 정렬하고 vw 단위로 수정하세요.
제안 diff
.framer-display-xxl {
font-family: 'Mona Sans', 'Inter', sans-serif;
font-size: clamp(52px, 8vw, 110px);
font-weight: 700;
line-height: 0.85;
- letter-spacing: clamp(-2.6px, -5%, -5.5px);
+ letter-spacing: clamp(-5.5px, -0.45vw, -2.6px);
font-feature-settings: 'ss02' on;
color: var(--framer-ink);
}
.framer-display-xl {
font-family: 'Mona Sans', 'Inter', sans-serif;
font-size: clamp(40px, 6vw, 85px);
font-weight: 700;
line-height: 0.95;
- letter-spacing: clamp(-2px, -5%, -4.25px);
+ letter-spacing: clamp(-4.25px, -0.35vw, -2px);
font-feature-settings: 'ss02' on;
color: var(--framer-ink);
}
.framer-display-lg {
font-family: 'Mona Sans', 'Inter', sans-serif;
font-size: clamp(32px, 4.5vw, 62px);
font-weight: 700;
line-height: 1.00;
- letter-spacing: clamp(-1.6px, -5%, -3.1px);
+ letter-spacing: clamp(-3.1px, -0.28vw, -1.6px);
font-feature-settings: 'ss02' on;
color: var(--framer-ink);
}📝 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.
| .framer-display-xxl { | |
| font-family: 'Mona Sans', 'Inter', sans-serif; | |
| font-size: clamp(52px, 8vw, 110px); | |
| font-weight: 700; | |
| line-height: 0.85; | |
| letter-spacing: clamp(-2.6px, -5%, -5.5px); | |
| font-feature-settings: 'ss02' on; | |
| color: var(--framer-ink); | |
| } | |
| .framer-display-xl { | |
| font-family: 'Mona Sans', 'Inter', sans-serif; | |
| font-size: clamp(40px, 6vw, 85px); | |
| font-weight: 700; | |
| line-height: 0.95; | |
| letter-spacing: clamp(-2px, -5%, -4.25px); | |
| font-feature-settings: 'ss02' on; | |
| color: var(--framer-ink); | |
| } | |
| .framer-display-lg { | |
| font-family: 'Mona Sans', 'Inter', sans-serif; | |
| font-size: clamp(32px, 4.5vw, 62px); | |
| font-weight: 700; | |
| line-height: 1.00; | |
| letter-spacing: clamp(-1.6px, -5%, -3.1px); | |
| font-feature-settings: 'ss02' on; | |
| color: var(--framer-ink); | |
| .framer-display-xxl { | |
| font-family: 'Mona Sans', 'Inter', sans-serif; | |
| font-size: clamp(52px, 8vw, 110px); | |
| font-weight: 700; | |
| line-height: 0.85; | |
| letter-spacing: clamp(-5.5px, -0.45vw, -2.6px); | |
| font-feature-settings: 'ss02' on; | |
| color: var(--framer-ink); | |
| } | |
| .framer-display-xl { | |
| font-family: 'Mona Sans', 'Inter', sans-serif; | |
| font-size: clamp(40px, 6vw, 85px); | |
| font-weight: 700; | |
| line-height: 0.95; | |
| letter-spacing: clamp(-4.25px, -0.35vw, -2px); | |
| font-feature-settings: 'ss02' on; | |
| color: var(--framer-ink); | |
| } | |
| .framer-display-lg { | |
| font-family: 'Mona Sans', 'Inter', sans-serif; | |
| font-size: clamp(32px, 4.5vw, 62px); | |
| font-weight: 700; | |
| line-height: 1.00; | |
| letter-spacing: clamp(-3.1px, -0.28vw, -1.6px); | |
| font-feature-settings: 'ss02' on; | |
| color: var(--framer-ink); | |
| } |
🧰 Tools
🪛 Stylelint (17.9.0)
[error] 76-76: Expected no quotes around "Inter" (font-family-name-quotes)
(font-family-name-quotes)
[error] 86-86: Expected no quotes around "Inter" (font-family-name-quotes)
(font-family-name-quotes)
[error] 96-96: Expected no quotes around "Inter" (font-family-name-quotes)
(font-family-name-quotes)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/globals.css` around lines 75 - 102, The letter-spacing clamp() arguments
in the .framer-display-xxl, .framer-display-xl, and .framer-display-lg rules are
inverted (min/max swapped) and use a percent for the preferred value, which
prevents responsive behavior; update each letter-spacing to use clamp(min,
preferred, max) with the preferred value expressed in vw (e.g., use a negative
vw value between the min and max) so the spacing scales responsively across
breakpoints; adjust the three selectors (.framer-display-xxl,
.framer-display-xl, .framer-display-lg) accordingly ensuring the smallest (most
negative) px is the first argument, a vw-based middle argument, and the largest
(least negative) px is the third argument.
| <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> | ||
| <SignInButton mode='redirect'> | ||
| <Button variant='ghost' size='sm' className='text-sm'> | ||
| <button className='btn-framer-secondary' style={{ padding: '8px 14px', minHeight: '36px' }}> | ||
| 로그인 | ||
| </Button> | ||
| </button> | ||
| </SignInButton> | ||
| <SignUpButton mode='redirect'> | ||
| <Button size='sm' className='bg-celadon-green hover:bg-celadon-green/90 text-sm'> | ||
| 회원가입 | ||
| </Button> | ||
| <button className='btn-framer-primary' style={{ padding: '8px 14px', minHeight: '36px' }}> | ||
| 시작하기 |
There was a problem hiding this comment.
헤더 인증 버튼의 터치 타깃을 다시 44px로 맞춰 주세요.
.btn-framer-* 기본값은 44px인데 여기서 minHeight: '36px'로 덮어써서 모바일 헤더에서 바로 접근성이 내려갑니다.
제안 diff
- <button className='btn-framer-secondary' style={{ padding: '8px 14px', minHeight: '36px' }}>
+ <button className='btn-framer-secondary' style={{ padding: '8px 14px', minHeight: '44px' }}>
로그인
</button>
@@
- <button className='btn-framer-primary' style={{ padding: '8px 14px', minHeight: '36px' }}>
+ <button className='btn-framer-primary' style={{ padding: '8px 14px', minHeight: '44px' }}>
시작하기
</button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/header/header-auth-section.tsx` around lines 37 - 45, The inline
style on the SignInButton and SignUpButton overrides the .btn-framer-* default
touch target by setting minHeight: '36px'; restore accessibility by removing
that override or changing minHeight to '44px' (or use height: '44px') on the
elements inside SignInButton and SignUpButton so the buttons using classNames
'btn-framer-secondary' and 'btn-framer-primary' have a 44px touch target.
| onMouseEnter={menu.subItems?.length ? () => handleMouseEnter(menu.key) : undefined} | ||
| onMouseLeave={menu.subItems?.length ? handleMouseLeave : undefined} | ||
| > |
There was a problem hiding this comment.
데스크톱 드롭다운이 마우스 hover에만 의존해 키보드 접근이 막혀 있습니다
Line 230-232는 hover로만 열고, Line 270-278에서 닫힌 상태는 invisible이라 키보드 사용자가 하위 메뉴에 진입할 수 없습니다. focus/blur 기반 열기/닫기를 추가해야 합니다.
제안 수정안
<div
key={menu.key}
className='relative'
onMouseEnter={menu.subItems?.length ? () => handleMouseEnter(menu.key) : undefined}
onMouseLeave={menu.subItems?.length ? handleMouseLeave : undefined}
+ onFocus={menu.subItems?.length ? () => handleMouseEnter(menu.key) : undefined}
+ onBlur={
+ menu.subItems?.length
+ ? e => {
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
+ handleMouseLeave()
+ }
+ }
+ : undefined
+ }
>Also applies to: 270-278
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/header/header.tsx` around lines 230 - 232, The desktop dropdown
currently only opens on hover (onMouseEnter/onMouseLeave using
handleMouseEnter(menu.key) and handleMouseLeave) which blocks keyboard users;
add corresponding onFocus and onBlur (or onFocusCapture/onBlurCapture if needed)
handlers to the same element(s) that have menu.subItems to call
handleMouseEnter(menu.key) and handleMouseLeave so focus opens the menu and blur
closes it, and update the visibility logic that applies the "invisible" class
(the block around lines 270-278) to consider the same open state used by hover
(e.g., the menu's isOpen/active state) so the submenu is not hidden when focused
via keyboard.
| style={{ | ||
| paddingLeft: '16px', | ||
| overflow: 'hidden', | ||
| transition: 'max-height 0.2s ease', | ||
| maxHeight: mobileDropdowns[menu.key] ? '400px' : '0', | ||
| }} |
There was a problem hiding this comment.
모바일 하위 메뉴가 고정 maxHeight 때문에 잘립니다
Line 434에서 열린 상태를 400px로 고정해 두어서, 항목이 많은 메뉴(예: 작품)는 하단 링크가 잘려 터치/클릭이 불가능해집니다. 열린 상태에서는 뷰포트 기준 높이 + 내부 스크롤로 바꿔야 합니다.
제안 수정안
style={{
paddingLeft: '16px',
- overflow: 'hidden',
+ overflowX: 'hidden',
+ overflowY: mobileDropdowns[menu.key] ? 'auto' : 'hidden',
transition: 'max-height 0.2s ease',
- maxHeight: mobileDropdowns[menu.key] ? '400px' : '0',
+ maxHeight: mobileDropdowns[menu.key] ? 'calc(100vh - 180px)' : '0',
}}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/header/header.tsx` around lines 430 - 435, The mobile submenu is
clipped because the open state uses a fixed maxHeight ('400px'); change the
style logic used where mobileDropdowns[menu.key] is read so the open state uses
a viewport-relative max (e.g., '80vh' or 'calc(100vh - <headerHeight>px)') and
enable internal scrolling (overflowY: 'auto') while keeping overflow: 'hidden'
for the closed state; update the style object around mobileDropdowns[menu.key]
to set maxHeight: mobileDropdowns[menu.key] ? '80vh' : '0' (or calc with your
header/footer offset) and add overflowY: mobileDropdowns[menu.key] ? 'auto' :
'hidden' to ensure tall menus can scroll instead of being cut off.
| --- | ||
| version: alpha | ||
| name: Framer | ||
| description: "A confident dark-canvas builder marketing site that treats the page like a working artboard — pure black surfaces, white display type set in GT Walsheim Medium with aggressive negative tracking, and a single confident blue (#0099ff) reserved for hyperlinks and selection states. The page rhythm is broken by oversized vibrant gradient atmosphere panels — magenta, violet, orange spotlights — that act as living showcase tiles, not decoration. Every CTA is a white pill on dark; every card is a translucent or charcoal surface; every section title pulls letter-spacing tight enough to feel like a poster." |
There was a problem hiding this comment.
문서 lint 경고를 같이 정리해 주세요.
프론트매터 뒤에 H1이 없고, ### Brand & Accent 이하 소제목들 앞뒤 빈 줄도 빠져 있어서 MD041/MD022 경고가 계속 남습니다. 문서 lint를 돌리는 저장소라면 이 파일만 계속 noise를 만들게 됩니다.
Also applies to: 256-296
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@DESIGN.md` around lines 1 - 4, The document is triggering MD041/MD022 because
there is no top-level H1 after the frontmatter and several subheadings (notably
"### Brand & Accent" and the headings under it) lack the required blank lines;
add a single H1 title line immediately after the YAML frontmatter (e.g., "#
Framer") and ensure there is a blank line before and after each subheading under
"### Brand & Accent" (and the same fixes for the block around lines 256-296) so
the linter no longer reports MD041/MD022.
| ### 1. sse-manager: `||` vs `??` (production bug) | ||
| ```ts | ||
| // Before (bug): 0 falsy → default 1000 | ||
| maxClients: options.maxClients || 1000 | ||
| // After (fix): | ||
| maxClients: options.maxClients ?? 1000 | ||
| ``` | ||
|
|
||
| ### 2. event-emitter `off()`: wrapped listener 추적 누락 | ||
| - `on()` 에서 `wrappedListener` 등록하지만 `off()` 에서 원본 listener 사용 | ||
| - → off()가 EventEmitter에서 못 찾음 → 리스너 잔존 | ||
| - Fix: `listenerWrappers` Map으로 원본 → wrapped 추적 | ||
|
|
||
| ### 3. WS/E2E test: `createEventEmitter()` (new instance) 사용 | ||
| - 프로덕션 매니저는 `getEventEmitter()` (singleton) 사용 | ||
| - test가 새 instance 만들어 emit → manager 못 받음 | ||
| - Fix: 모든 test에서 `getEventEmitter()` 사용 | ||
|
|
||
| ## §3. Fix Pattern (4 files, ~20 hunks) | ||
|
|
||
| | File | Type | Hunks | | ||
| |---|---|---| | ||
| | `lib/realtime/sse-manager.ts` | source | 3 (`||` → `??`) | | ||
| | `lib/realtime/event-emitter.ts` | source | 3 (Map field + on/off update) | | ||
| | `lib/realtime/__tests__/websocket-manager.test.ts` | test | 4 (createEventEmitter → getEventEmitter) | | ||
| | `lib/realtime/__tests__/e2e-flow.test.ts` | test | 12 (replace_all) | |
There was a problem hiding this comment.
Markdownlint 경고를 정리하세요.
헤딩/펜스 앞뒤 빈 줄이 없고, 50행 표의 || 때문에 열 개수 오류가 납니다. 문서 lint가 CI에 포함되면 이 파일은 실패하므로 \|\|로 이스케이프하고 빈 줄을 추가해 주세요.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 28-28: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 29-29: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 36-36: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 41-41: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 50-50: Table column count
Expected: 3; Actual: 5; Too many cells, extra data will be missing
(MD056, table-column-count)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/01-plan/features/tests-realtime-logic-fixes.plan.md` around lines 28 -
53, Add blank lines before and after each heading and fenced code block, and
escape the pipe characters in the table header row by replacing "||" with "\|\|"
in the markdown line that shows the bug example (specifically the snippet
"maxClients: options.maxClients || 1000"); ensure the code fence blocks (```ts)
have an empty line above and below and that the table row using "||" is changed
to use "\|\|" so markdownlint no longer reports missing surrounding blank lines
or a malformed table column count.
| import { getEventEmitter, EventType, type EventPayload } from '../event-emitter' | ||
| import { createSubscriptionManager, ConnectionType } from '../subscription-manager' |
There was a problem hiding this comment.
getEventEmitter() 싱글톤은 테스트마다 초기화해야 합니다.
이 파일은 여러 케이스에서 공유 emitter를 쓰는데 afterEach에 shutdown()/reset이 없어 이전 케이스의 구독과 리스너가 다음 테스트로 새어 나갈 수 있습니다. afterEach에서 emitter 정리를 추가해 테스트 격리를 보장하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/realtime/__tests__/e2e-flow.test.ts` around lines 16 - 17, The shared
getEventEmitter() singleton is not being reset between tests, causing
subscriptions/listeners to leak; add an afterEach hook that fetches the
singleton via getEventEmitter() and calls its shutdown() or reset method (or
removeAllListeners()/clearSubscriptions() if shutdown is not available) to tear
down subscriptions created during each test and ensure test isolation for tests
that use createSubscriptionManager and EventType/EventPayload.
| private redisChannel: string | ||
| private subscriptions: Map<string, Set<EventListener>> | ||
| // 원본 listener → wrapped listener 추적 (off() 에서 wrapped 찾기 위해 필요) | ||
| private listenerWrappers: Map<EventListener, EventListener> = new Map() |
There was a problem hiding this comment.
listenerWrappers를 원본 함수 하나로만 추적하면 구독이 덮어써집니다.
같은 콜백을 다른 이벤트나 필터로 여러 번 등록하면 마지막 wrapper만 남고, off()는 잘못된 구독만 제거합니다. (type, listener) 기준으로 추적하거나 중첩 Map으로 바꾸고, removeAllListeners() 시 wrapper map도 함께 비워야 합니다.
Also applies to: 240-296
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/realtime/event-emitter.ts` at line 111, listenerWrappers currently maps
only by the original EventListener, so registering the same callback for
different event types/filters overwrites previous wrappers and off() removes the
wrong subscription; change the tracking to include the event identity (e.g., use
a composite key of type+listener+filter or a nested Map keyed by event type then
listener then filter) and update the add/on, off, and removeAllListeners
implementations (referencing listenerWrappers, off(), removeAllListeners(), and
the addListener/on code around lines ~240-296) to look up and remove the exact
wrapper for the specific (type, listener, filter) combination, and ensure
removeAllListeners() clears the listenerWrappers map for the removed scope.
| keepAliveInterval: options.keepAliveInterval ?? 30000, // 30 seconds | ||
| clientTimeout: options.clientTimeout ?? 300000, // 5 minutes | ||
| maxClients: options.maxClients ?? 1000, // ?? not || (allow 0 for testing) |
There was a problem hiding this comment.
keepAliveInterval/clientTimeout까지 ??로 바꾸면 위험합니다.
0이 들어오면 keepAliveInterval은 setInterval(0)로 이벤트 루프를 과도하게 돌리고, clientTimeout은 클라이언트를 즉시 만료시킵니다. 이번 수정은 maxClients에만 필요했으니, 두 타이머 옵션은 기존 동작을 유지하거나 0에 대한 명시적 검증을 넣어 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/realtime/sse-manager.ts` around lines 80 - 82, The change to use nullish
coalescing for keepAliveInterval and clientTimeout is unsafe because clients can
pass 0; restore safe behavior by not treating 0 as a valid override for these
timers (unlike maxClients). Specifically, update the assignment logic for
keepAliveInterval and clientTimeout (symbols: keepAliveInterval, clientTimeout,
options) to either keep the original falsy-default behavior or explicitly
validate that provided values are positive numbers (e.g., use
options.keepAliveInterval only if it is a number > 0, otherwise use 30000; same
for clientTimeout with 300000); leave maxClients using ?? so 0 remains allowed.
- page.tsx: canvas 배경, display-lg 헤드라인, framer-eyebrow 칩, white/dark pill CTA, hairline 구분선, surface-1 서비스 카드 - GalleryClient: bg-white → framer-canvas 배경 교체 - GalleryGrid: - 검색 바: surface-1 + pill 형태, accent-blue 포커스 링 - 카테고리 필터: white pill (활성) / surface-1 pill (비활성) - 갤러리 카드: surface-1 배경, surface-2 스켈레톤 - 카테고리 배지: dark glassmorphism eyebrow 칩 - 라이트박스: surface-1 패널, canvas 이미지 배경 - 하단 정보: framer-caption, surface-2 태그 칩
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
components/gallery/GalleryGrid.tsx (3)
68-68: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win미사용 상태 변수
isLoading제거 필요
isLoading상태가 선언되었지만 컴포넌트 내에서 사용되지 않습니다. 불필요한 코드이므로 제거하세요.🧹 제거 제안
- const [isLoading, setIsLoading] = useState(false)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/gallery/GalleryGrid.tsx` at line 68, 컴포넌트에서 선언만 되고 사용되지 않는 상태 변수 isLoading 및 해당 세터 setIsLoading을 제거하세요: GalleryGrid 컴포넌트에서 const [isLoading, setIsLoading] = useState(false) 선언을 삭제하고 더 이상 참조되지 않는 경우 useState import도 정리(또는 다른 사용처가 없으면 제거)해 불필요한 코드를 제거합니다.
454-502:⚠️ Potential issue | 🔴 Critical | ⚡ Quick win액션 버튼들이
animate={{ scale: 0 }}로 인해 영구적으로 보이지 않음
motion.button요소들이animate={{ scale: 0, rotate: 45 }}로 설정되어 있어 버튼이 항상 0 크기로 렌더링됩니다.animate속성은 요소의 기본 상태를 정의하므로, 부모 컨테이너가group-hover로 보여지더라도 버튼 자체는 크기가 0이라 hover 및 클릭이 불가능합니다.
whileHover는 요소 위에 마우스가 올라갈 때만 적용되는데,scale: 0인 요소는 hover 가능한 영역이 없습니다.🐛 수정 제안: animate에서 scale을 1로 변경
<motion.button initial={{ scale: 0, rotate: 45 }} - animate={{ scale: 0, rotate: 45 }} - whileHover={{ scale: 1.1, rotate: 0 }} + animate={{ scale: 1, rotate: 0 }} + whileHover={{ scale: 1.1 }} className='p-2.5 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm rounded-full shadow-lg hover:bg-white dark:hover:bg-gray-800 hover:shadow-xl transition-all duration-300' onClick={e => handleShareClick(e, item)} aria-label='공유하기'또는 부모 hover 시에만 버튼이 나타나게 하려면:
<motion.button - initial={{ scale: 0, rotate: 45 }} - animate={{ scale: 0, rotate: 45 }} - whileHover={{ scale: 1.1, rotate: 0 }} + initial={{ opacity: 0, scale: 0.8 }} + whileInView={{ opacity: 1, scale: 1 }} + whileHover={{ scale: 1.1 }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/gallery/GalleryGrid.tsx` around lines 454 - 502, The two motion.button instances use animate={{ scale: 0, rotate: 45 }} which leaves them permanently hidden; change animate to show the default visible state (e.g., animate={{ scale: 1, rotate: 0 }}) or otherwise set animate to the non-zero visible values so the buttons can receive hover/click events; update both motion.button elements (the ones calling handleShareClick and handleImageClick) to use the visible animate values and keep whileHover for the hover transform, and ensure e.stopPropagation remains on the image fullscreen button.
356-364: 🧹 Nitpick | 🔵 Trivial | 💤 Low value가상화된 그리드에서 인덱스 기반 애니메이션 지연 검토
delay: index * 0.08은 VirtuosoGrid의 가상화와 함께 사용될 때 예상치 못한 동작을 유발할 수 있습니다. 사용자가 스크롤하면 새로 보이는 항목들이 원본 인덱스 기반으로 지연되어, 예를 들어 50번째 항목이 4초 지연 후 나타날 수 있습니다.고정된 짧은 지연 또는
whileInView사용을 고려해보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/gallery/GalleryGrid.tsx` around lines 356 - 364, The delay computed from the original item index (delay: index * 0.08) causes long, unexpected delays with virtualized lists; update the motion props in GalleryGrid (the element using initial/animate/exit/transition and the transition.delay that references index) to use a fixed short delay or a viewport-based trigger instead of index-based delay—for example remove index-based multiplication and use a constant delay (e.g., 0.04) or switch to a whileInView / inView-driven animation so newly-rendered items animate when they enter the viewport rather than by their original index.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/gallery/page.tsx`:
- Around line 201-209: The inline style on the <p> element uses both marginTop:
'20px' and margin: '20px auto 0', causing marginTop to be overwritten; update
the style for the element with className 'framer-body-lg framer-fade-up
framer-fade-up-delay-1' to remove the redundant marginTop and keep a single
source of truth (either use margin shorthand '20px auto 0' only or replace the
shorthand with explicit marginTop if centering isn't required), ensuring the
intended spacing is preserved.
In `@components/gallery/GalleryClient.tsx`:
- Around line 12-19: handleGalleryEvent currently uses event: any and accesses
event.payload.category and event.payload.itemId without null checks; define a
typed interface (e.g., GalleryEvent with optional payload?: { category?: string;
itemId?: string }) and change the handler signature to use that type, then guard
accesses using optional chaining and a safe fallback for event_label (for
example event.payload?.category ?? event.payload?.itemId ?? 'unknown'); keep the
window.gtag guard but ensure you only call it if the resolved label is
non-empty.
In `@components/gallery/GalleryGrid.tsx`:
- Around line 390-398: The inline style uses animation: 'pulse ...' but no
`@keyframes` are defined so the pulse never runs; update the three affected
components (GalleryGrid component, HeaderAuthSection component, and GalleryPage
component) to remove the inline animation and instead apply Tailwind's built-in
animate-pulse utility (e.g., add the class 'animate-pulse' alongside the
existing classes), or alternatively add a named 'pulse' keyframes entry and
animation in tailwind.config.ts under theme.extend.keyframes/animation and
rebuild so the inline animation string matches that name; ensure the replacement
is applied where the skeleton loader divs are rendered (the skeleton container
in GalleryGrid, the loader in header-auth-section, and the loader in app gallery
page).
---
Outside diff comments:
In `@components/gallery/GalleryGrid.tsx`:
- Line 68: 컴포넌트에서 선언만 되고 사용되지 않는 상태 변수 isLoading 및 해당 세터 setIsLoading을 제거하세요:
GalleryGrid 컴포넌트에서 const [isLoading, setIsLoading] = useState(false) 선언을 삭제하고 더
이상 참조되지 않는 경우 useState import도 정리(또는 다른 사용처가 없으면 제거)해 불필요한 코드를 제거합니다.
- Around line 454-502: The two motion.button instances use animate={{ scale: 0,
rotate: 45 }} which leaves them permanently hidden; change animate to show the
default visible state (e.g., animate={{ scale: 1, rotate: 0 }}) or otherwise set
animate to the non-zero visible values so the buttons can receive hover/click
events; update both motion.button elements (the ones calling handleShareClick
and handleImageClick) to use the visible animate values and keep whileHover for
the hover transform, and ensure e.stopPropagation remains on the image
fullscreen button.
- Around line 356-364: The delay computed from the original item index (delay:
index * 0.08) causes long, unexpected delays with virtualized lists; update the
motion props in GalleryGrid (the element using initial/animate/exit/transition
and the transition.delay that references index) to use a fixed short delay or a
viewport-based trigger instead of index-based delay—for example remove
index-based multiplication and use a constant delay (e.g., 0.04) or switch to a
whileInView / inView-driven animation so newly-rendered items animate when they
enter the viewport rather than by their original index.
🪄 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: 5c66e70f-e353-489d-a626-f1c5122e9478
📒 Files selected for processing (3)
app/gallery/page.tsxcomponents/gallery/GalleryClient.tsxcomponents/gallery/GalleryGrid.tsx
| {/* Subhead */} | ||
| <p | ||
| className='framer-body-lg framer-fade-up framer-fade-up-delay-1' | ||
| style={{ marginTop: '20px', maxWidth: '520px', margin: '20px auto 0' }} | ||
| > | ||
| 正法의 계승, 創新의 조화 | ||
| <br /> | ||
| 전통 서예의 정법을 계승하고 현대적 창신을 통해 동양서예의 새로운 미래를 열어갑니다 | ||
| </p> |
There was a problem hiding this comment.
스타일 속성 충돌: marginTop이 margin에 의해 덮어씌워짐
marginTop: '20px'이 선언된 후 바로 margin: '20px auto 0'에 의해 덮어씌워집니다. 이는 혼란을 주고 의도하지 않은 결과를 초래할 수 있습니다.
🧹 수정 제안
<p
className='framer-body-lg framer-fade-up framer-fade-up-delay-1'
- style={{ marginTop: '20px', maxWidth: '520px', margin: '20px auto 0' }}
+ style={{ maxWidth: '520px', margin: '20px auto 0' }}
>📝 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.
| {/* Subhead */} | |
| <p | |
| className='framer-body-lg framer-fade-up framer-fade-up-delay-1' | |
| style={{ marginTop: '20px', maxWidth: '520px', margin: '20px auto 0' }} | |
| > | |
| 正法의 계승, 創新의 조화 | |
| <br /> | |
| 전통 서예의 정법을 계승하고 현대적 창신을 통해 동양서예의 새로운 미래를 열어갑니다 | |
| </p> | |
| {/* Subhead */} | |
| <p | |
| className='framer-body-lg framer-fade-up framer-fade-up-delay-1' | |
| style={{ maxWidth: '520px', margin: '20px auto 0' }} | |
| > | |
| 正法의 계승, 創新의 조화 | |
| <br /> | |
| 전통 서예의 정법을 계승하고 현대적 창신을 통해 동양서예의 새로운 미래를 열어갑니다 | |
| </p> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/gallery/page.tsx` around lines 201 - 209, The inline style on the <p>
element uses both marginTop: '20px' and margin: '20px auto 0', causing marginTop
to be overwritten; update the style for the element with className
'framer-body-lg framer-fade-up framer-fade-up-delay-1' to remove the redundant
marginTop and keep a single source of truth (either use margin shorthand '20px
auto 0' only or replace the shorthand with explicit marginTop if centering isn't
required), ensuring the intended spacing is preserved.
| const handleGalleryEvent = useCallback((event: any) => { | ||
| // 엔터프라이즈 아키텍처의 이벤트 시스템과 연동 | ||
| if (typeof window !== 'undefined' && window.gtag) { | ||
| window.gtag('event', event.type, { | ||
| event_category: 'Gallery', | ||
| event_label: event.payload.category || event.payload.itemId, | ||
| custom_map: { | ||
| gallery_event: event.type, | ||
| }, | ||
| }) | ||
| } | ||
| }, []) |
There was a problem hiding this comment.
event.payload 접근 시 방어적 null 체크 필요
event.payload.category 및 event.payload.itemId에 접근하기 전에 event.payload가 존재하는지 확인하지 않습니다. payload가 없는 이벤트가 발생하면 런타임 에러가 발생할 수 있습니다.
또한 event: any 타입은 TypeScript의 타입 안전성을 우회합니다. 갤러리 이벤트용 인터페이스 정의를 고려해보세요.
🛡️ 방어적 코딩 제안
+ interface GalleryEvent {
+ type: string
+ payload?: {
+ category?: string
+ itemId?: string
+ }
+ }
+
const handleGalleryEvent = useCallback((event: any) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', event.type, {
event_category: 'Gallery',
- event_label: event.payload.category || event.payload.itemId,
+ event_label: event.payload?.category || event.payload?.itemId || 'unknown',
})
}
}, [])📝 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 handleGalleryEvent = useCallback((event: any) => { | |
| // 엔터프라이즈 아키텍처의 이벤트 시스템과 연동 | |
| if (typeof window !== 'undefined' && window.gtag) { | |
| window.gtag('event', event.type, { | |
| event_category: 'Gallery', | |
| event_label: event.payload.category || event.payload.itemId, | |
| custom_map: { | |
| gallery_event: event.type, | |
| }, | |
| }) | |
| } | |
| }, []) | |
| interface GalleryEvent { | |
| type: string | |
| payload?: { | |
| category?: string | |
| itemId?: string | |
| } | |
| } | |
| const handleGalleryEvent = useCallback((event: GalleryEvent) => { | |
| if (typeof window !== 'undefined' && window.gtag) { | |
| window.gtag('event', event.type, { | |
| event_category: 'Gallery', | |
| event_label: event.payload?.category || event.payload?.itemId || 'unknown', | |
| }) | |
| } | |
| }, []) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/gallery/GalleryClient.tsx` around lines 12 - 19,
handleGalleryEvent currently uses event: any and accesses event.payload.category
and event.payload.itemId without null checks; define a typed interface (e.g.,
GalleryEvent with optional payload?: { category?: string; itemId?: string }) and
change the handler signature to use that type, then guard accesses using
optional chaining and a safe fallback for event_label (for example
event.payload?.category ?? event.payload?.itemId ?? 'unknown'); keep the
window.gtag guard but ensure you only call it if the resolved label is
non-empty.
| {/* 스켈레톤 로더 — Framer surface-2 */} | ||
| {!isImageLoaded && !hasImageError && ( | ||
| <div className='absolute inset-0 animate-pulse bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200 dark:from-gray-700 dark:via-gray-600 dark:to-gray-700 bg-[length:200%_100%] animate-shimmer'> | ||
| <div className='absolute inset-0 flex items-center justify-center'> | ||
| <svg | ||
| className='w-12 h-12 text-gray-400 dark:text-gray-500 animate-spin' | ||
| fill='none' | ||
| viewBox='0 0 24 24' | ||
| > | ||
| <circle | ||
| className='opacity-25' | ||
| cx='12' | ||
| cy='12' | ||
| r='10' | ||
| stroke='currentColor' | ||
| strokeWidth='4' | ||
| ></circle> | ||
| <path | ||
| className='opacity-75' | ||
| fill='currentColor' | ||
| d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z' | ||
| ></path> | ||
| </svg> | ||
| </div> | ||
| </div> | ||
| <div | ||
| style={{ | ||
| position: 'absolute', inset: 0, | ||
| backgroundColor: 'var(--framer-surface-2)', | ||
| animation: 'pulse 1.5s ease-in-out infinite', | ||
| }} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# pulse keyframes 정의 검색
rg -n "@keyframes\s+pulse" --type=cssRepository: jlinsights/ASCA
Length of output: 41
🏁 Script executed:
#!/bin/bash
# Search for pulse keyframes definition in all file types
echo "=== Searching for pulse keyframes (all file types) ==="
rg -n "pulse" -t css -t ts -t tsx -t js --type-add="css:*.css" --type-add="css:*.scss" --type-add="css:*.sass" | grep -i "keyframe\|@keyframe\|pulse.*{" | head -20
echo ""
echo "=== Checking if GalleryGrid.tsx exists ==="
fd "GalleryGrid.tsx"
echo ""
echo "=== Checking for globals.css or global styles ==="
fd -e css -e scss -e sass | grep -i "global\|index\|style" | head -10
echo ""
echo "=== Checking for Tailwind configuration ==="
fd "tailwind.config" -e js -e ts -e json
echo ""
echo "=== Searching for 'animate-pulse' usage ==="
rg "animate-pulse" --type ts --type tsx | head -5Repository: jlinsights/ASCA
Length of output: 509
🏁 Script executed:
#!/bin/bash
# Check the actual file content around the problematic lines
echo "=== Checking GalleryGrid.tsx around lines 390-398 ==="
sed -n '380,400p' components/gallery/GalleryGrid.tsx
echo ""
echo "=== Searching for pulse definition in CSS files ==="
echo "app/globals.css:"
grep -n "pulse" app/globals.css || echo "No pulse found"
echo ""
echo "styles/globals.css:"
grep -n "pulse" styles/globals.css || echo "No pulse found"
echo ""
echo "styles/gallery.css:"
grep -n "pulse" styles/gallery.css || echo "No pulse found"
echo ""
echo "styles/enhanced-gallery.css:"
grep -n "pulse" styles/enhanced-gallery.css || echo "No pulse found"
echo ""
echo "=== Checking Tailwind config for animation definitions ==="
cat tailwind.config.ts | grep -A 10 "animation"
echo ""
echo "=== Searching for keyframes definitions anywhere in codebase ==="
rg -n "keyframes" | head -10Repository: jlinsights/ASCA
Length of output: 3157
🏁 Script executed:
#!/bin/bash
# Check if Tailwind has built-in animate-pulse and verify keyframes definition
echo "=== Checking full keyframes section in tailwind.config.ts ==="
sed -n '124,200p' tailwind.config.ts
echo ""
echo "=== Verify no pulse in any CSS file ==="
rg "pulse" --type-list | head -5
grep -r "pulse" styles/ app/globals.css 2>/dev/null | grep -v node_modules || echo "No pulse found in CSS files"Repository: jlinsights/ASCA
Length of output: 2804
🏁 Script executed:
#!/bin/bash
# Check if Tailwind has a default animate-pulse that might provide pulse keyframes
echo "=== Check all animation definitions more thoroughly ==="
sed -n '120,220p' tailwind.config.ts
echo ""
echo "=== Check if there's any CSS animation defined inline or elsewhere ==="
rg "animation.*pulse" --all-types
echo ""
echo "=== Check the imports in GalleryGrid.tsx to see what styles are used ==="
head -30 components/gallery/GalleryGrid.tsx | grep -i "import\|css\|style"Repository: jlinsights/ASCA
Length of output: 3805
🏁 Script executed:
#!/bin/bash
# Do a comprehensive search for pulse in all files, including looking at full content
echo "=== Search for 'pulse' in entire codebase ==="
rg "pulse" -i
echo ""
echo "=== Check the full content of enhanced-gallery.css to ensure pulse isn't there ==="
wc -l styles/enhanced-gallery.css
echo ""
echo "=== Look for any CSS that might define pulse elsewhere ==="
rg "@keyframes" -A 5 | grep -B 5 "pulse"Repository: jlinsights/ASCA
Length of output: 12410
🏁 Script executed:
#!/bin/bash
# Verify the exact occurrences of inline pulse animation
echo "=== GalleryGrid.tsx with inline pulse animation ==="
rg -n "animation.*pulse" components/gallery/GalleryGrid.tsx -B 2 -A 2
echo ""
echo "=== header-auth-section.tsx with inline pulse animation ==="
rg -n "animation.*pulse" components/header/header-auth-section.tsx -B 2 -A 2
echo ""
echo "=== app/gallery/page.tsx with inline pulse animation ==="
rg -n "animation.*pulse" app/gallery/page.tsx -B 2 -A 2
echo ""
echo "=== Final check: is there ANY `@keyframes` pulse definition? ==="
rg "@keyframes\s+pulse|pulse\s*:\s*{" -A 3Repository: jlinsights/ASCA
Length of output: 1051
🏁 Script executed:
#!/bin/bash
# One more comprehensive check - look for any styles that might provide pulse
echo "=== Search for 'pulse' definition in CSS/SCSS files with context ==="
find . -type f \( -name "*.css" -o -name "*.scss" -o -name "*.sass" \) -not -path "*/node_modules/*" -not -path "*/.next/*" -exec grep -l "pulse" {} \;
echo ""
echo "=== Check if browser/Tailwind has default pulse animation ==="
echo "Checking if 'pulse' is a standard CSS animation (it's not - it's a Tailwind utility)"
echo ""
echo "=== List all Tailwind animations in config ==="
grep -A 100 "animation:" tailwind.config.ts | grep "'" | head -20Repository: jlinsights/ASCA
Length of output: 1532
인라인 pulse 애니메이션 keyframes 정의 누락 — 애니메이션 동작 불가
코드에서 animation: 'pulse 1.5s ease-in-out infinite'를 사용하고 있으나, 대응하는 CSS @keyframes pulse 정의가 어디에도 없습니다. 이 애니메이션은 브라우저에서 인식되지 않아 작동하지 않으며 콘솔에 경고를 발생시킵니다.
세 곳에서 영향을 받습니다:
components/gallery/GalleryGrid.tsx(라인 396)components/header/header-auth-section.tsx(라인 29)app/gallery/page.tsx(라인 42)
해결 방법: Tailwind의 animate-pulse 클래스를 사용하거나, tailwind.config.ts에 pulse keyframes을 추가하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/gallery/GalleryGrid.tsx` around lines 390 - 398, The inline style
uses animation: 'pulse ...' but no `@keyframes` are defined so the pulse never
runs; update the three affected components (GalleryGrid component,
HeaderAuthSection component, and GalleryPage component) to remove the inline
animation and instead apply Tailwind's built-in animate-pulse utility (e.g., add
the class 'animate-pulse' alongside the existing classes), or alternatively add
a named 'pulse' keyframes entry and animation in tailwind.config.ts under
theme.extend.keyframes/animation and rebuild so the inline animation string
matches that name; ensure the replacement is applied where the skeleton loader
divs are rendered (the skeleton container in GalleryGrid, the loader in
header-auth-section, and the loader in app gallery page).
## 원인 분석 - loadedImages: 1,930개 항목 Record 사전 생성 → Set으로 교체 (초기화 비용 제거) - motion.div layout + stagger delay (index*80ms) → plain div + CSS fadeInUp으로 교체 - GalleryClient data prop: 1.7MB JSON Server→Client 직렬화 → Client 직접 import - imageSizes 15개 → 5개로 축소 (Next.js 최적화 경로 결정 간소화) - minimumCacheTTL 24h → 7d - useWindowScroll + overscan 200→600px - VirtuosoGrid height fixed → useWindowScroll 네이티브 스크롤 - quality 85 → 75 (그리드 썸네일), sizes 정확화 - blur-md 클래스 제거 (GPU 과부하 방지) ## 측정 결과 (dev 서버, 컴파일 후) - 2nd request: 2.099s → 0.296s (-85%) - 3rd request: 0.165s (-92%)
Summary
🎯 부모 사이클 `tests-realtime-async-fix` ejection 정식 처리. realtime 116/116 PASS 달성.
3 sub-cause → source 2 + test 2 file fix:
Root Cause 1: sse-manager production bug
`options.maxClients || 1000` → 0이 falsy → default 1000 됨
Fix: `||` → `??` (nullish coalescing)
Root Cause 2: event-emitter wrapper 추적 누락
on()이 wrappedListener 등록하지만 off()는 원본 listener 사용 → 못 찾음
Fix: `listenerWrappers` Map으로 원본 → wrapped 추적
Root Cause 3: test singleton mismatch
프로덕션 매니저는 getEventEmitter() (singleton), 테스트는 createEventEmitter() (new)
Fix: 테스트도 getEventEmitter() 사용
Test plan
rev β 12연속 (Match avg 97%+).
Summary by CodeRabbit
새로운 기능
버그 수정
문서