Skip to content

Comments

[Feat/404-48] ✨ feat: nofound 페이지 구현 & wipCard 추가 (준비중 안내)#100

Merged
swallowedB merged 4 commits intodevfrom
feat/404-48
Jan 14, 2026
Merged

[Feat/404-48] ✨ feat: nofound 페이지 구현 & wipCard 추가 (준비중 안내)#100
swallowedB merged 4 commits intodevfrom
feat/404-48

Conversation

@swallowedB
Copy link
Owner

@swallowedB swallowedB commented Jan 14, 2026

요약

  • 변경 목적(왜?): 404 페이지 부재 및 준비중 기능 접근 시 사용자 경험 보완
  • 주요 변경(무엇을?): NotFound 페이지 구현, WIP Card(준비중 안내) 추가, 임시 일러스트 적용

변경 내용

  • UI/컴포넌트
    • app/not-found.tsx 추가 → 404 페이지 구현
    • WipFeatureCard 컴포넌트 추가 (준비중 안내)
    • WipFeatureOverlay 구현 → 준비중 기능 오버레이 표시
    • 임시 일러스트(public/error/wip-card.svg) 적용
  • 로직/유틸
    • WIP 상태 토글 훅(useWipFeatureNotice) 연결
    • 비공개 기능 클릭 시 오버레이 노출
  • 문서/설정 (해당 없음)

스크린샷/동영상 (선택)

스크린샷 2026-01-15 오전 12 11 35 스크린샷 2026-01-15 오전 12 11 54

테스트

  • 404 접근 시 정상 렌더링 확인
  • 준비중 기능 클릭 시 오버레이 표시 확인
  • 오버레이 바깥 클릭 및 X 버튼 동작 확인
  • 유닛 테스트 (추후 작성)

관련 이슈

close #48

Summary by CodeRabbit

릴리스 노트

  • 새 기능

    • 작업 중(WIP) 기능 알림 오버레이 추가(일부 메뉴 클릭 시 표시, 자동 닫힘)
    • 전용 404 페이지 추가
    • 우주 배경 구성요소 및 유성/별 애니메이션 추가
  • 개선 사항

    • 도크/헤더 메뉴 항목에서 작업 중 표시 트리거 가능(특정 항목 클릭 시)
    • 미디어 렌더링 간소화 및 이미지 처리 최적화

✏️ Tip: You can customize this high-level summary in your review settings.

@swallowedB swallowedB linked an issue Jan 14, 2026 that may be closed by this pull request
3 tasks
@vercel
Copy link

vercel bot commented Jan 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
b0o0a Ready Ready Preview, Comment Jan 14, 2026 3:30pm

@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

WIP 기능 알림 훅과 오버레이 컴포넌트, Space 배경 스타일과 컴포넌트, 글로벌 404 페이지(NotFound)를 추가했습니다. SiteHeader와 DockMenu에 WIP 알림 트리거가 통합되고 DockMenuItem에 선택적 onClick prop이 도입되었습니다.

Changes

Cohort / File(s) 요약
WIP 훅
src/hooks/useWipFeatureNotice.ts
WIP 알림 상태(isOpen)와 openNotice, closeNotice를 제공하는 훅 추가 (autoCloseMs 옵션 포함)
WIP UI 컴포넌트
src/components/common/WipFeatureOverlay.tsx, src/components/common/WipFeatureCard.tsx
포탈 기반 오버레이와 카드 컴포넌트 추가, framer-motion 애니메이션 및 backdrop 클릭으로 닫기 처리
Dock 관련 변경
src/app/(layout)/(shell)/_components/dock/DockMenu.tsx, src/app/(layout)/(shell)/_components/dock/DockMenuItem.tsx, src/app/(layout)/(shell)/_components/dock/dock.types.ts
DockMenuItem에 선택적 onClick prop 추가; DockMenu에서 PHOTOBOOTH 클릭 시 openNotice() 호출로 WIP 오버레이 노출
헤더 통합
src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx
기존 네비게이션 링크(Resume, Guestbook, Lab)를 WIP 알림을 여는 버튼으로 변경; 모바일 메뉴 항목도 동일 처리
404 페이지 추가
src/app/not-found.tsx
글로벌 404 페이지 컴포넌트 추가 (NotFound 기본 export), SpaceBackground 및 홈 이동 버튼 포함
배경 컴포넌트 및 스타일
src/components/common/SpaceBackground.tsx, src/styles/components/background.css, src/app/globals.css
우주 배경 React 컴포넌트 추가 및 배경 애니메이션 CSS를 글로벌에 임포트
HeroRightMedia 리팩토링
src/app/(layout)/(shell)/_components/home/HeroRightMedia.tsx
복잡한 배경을 Next.js Image로 단순화

Sequence Diagram

sequenceDiagram
    actor User
    participant DockMenu
    participant useWipFeatureNotice
    participant WipFeatureOverlay
    participant WipFeatureCard

    User->>DockMenu: PHOTOBOOTH 클릭
    DockMenu->>useWipFeatureNotice: openNotice()
    useWipFeatureNotice->>useWipFeatureNotice: isOpen = true (auto-close 타이머 설정)
    useWipFeatureNotice-->>DockMenu: 상태 변경 반영
    DockMenu->>WipFeatureOverlay: isOpen={true}
    WipFeatureOverlay->>WipFeatureCard: 카드 렌더링 (애니메이션)
    User->>WipFeatureCard: 닫기 버튼 클릭
    WipFeatureCard->>WipFeatureOverlay: onClose()
    WipFeatureOverlay->>useWipFeatureNotice: closeNotice()
    useWipFeatureNotice->>useWipFeatureNotice: isOpen = false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🌌✨
404가 길을 잃어도 걱정 마요
WIP 알림이 살짝 손을 흔들고
별들이 부드럽게 배경을 수놓네
클릭 한 번에 안내가 반짝—🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 3
❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive PR은 #48의 주요 요구사항(404 페이지, WIP 오버레이)을 구현했으나, error.tsx는 미포함되어 부분적으로만 충족됩니다. error.tsx 구현 계획을 명시하거나, 후속 PR로 분리하는 것을 권장합니다. 현 PR이 #48을 전체 완료하는지 확인 필요.
Out of Scope Changes check ❓ Inconclusive HeroRightMedia.tsx의 Image 변경(elephant.svg로 교체)이 #48 요구사항과 연결되지 않는 것으로 보입니다. HeroRightMedia.tsx 변경이 어느 목적/이슈와 관련된 변경인지 명시 필요. 404 페이지/WIP Card와의 연관성 확인.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 변경 내용의 핵심(404 페이지와 WIP Card 추가)을 명확히 설명하고 있습니다.
Description check ✅ Passed 설명이 템플릿 구조를 따르며, 변경 목적, 주요 변경 사항, 테스트 내용을 충실히 담고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ca740e and 30a046c.

📒 Files selected for processing (4)
  • src/app/(layout)/(shell)/_components/dock/DockMenu.tsx
  • src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx
  • src/app/not-found.tsx
  • src/hooks/useWipFeatureNotice.ts

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In `@src/app/`(layout)/(shell)/_components/dock/DockMenu.tsx:
- Around line 90-93: Replace the fragile magic-string check in handleItemClick
by introducing an explicit isWip flag on the dock item config and using that
flag for behavior: add isWip?: boolean to DOCK_ITEMS entries (e.g., the
PHOTOBOOTH button), update the type for dock items to include isWip, and change
the handleItemClick logic to use item.isWip (combined with type/button check) to
decide whether to call openNotice instead of comparing item.label to
"PHOTOBOOTH".

In `@src/app/`(layout)/(shell)/_components/home/HeroRightMedia.tsx:
- Around line 8-15: Update the Image in the HeroRightMedia component: replace
the non-descriptive alt "임시" with a meaningful description of the illustration
(e.g., describe the elephant illustration’s purpose) or set alt="" if the
graphic is purely decorative; verify whether the intended asset is
"/error/elephant.svg" or "/error/wip-card.svg" and switch the src to the correct
file if needed; and remove the explicit priority={false} prop since it’s the
Next.js default.

In `@src/app/`(layout)/(shell)/_components/layout/SiteHeader.tsx:
- Around line 68-95: Replace the three repeated <li> button blocks in SiteHeader
by extracting labels into a constant array (e.g. WIP_NAV_ITEMS =
["Resume","Guestbook","Lab"]) and render them with .map(), returning an <li
key={label} className="hover:text-foreground"> containing the <button
type="button" onClick={handleClick} className="text-sm
cursor-pointer">{label}</button>; ensure each mapped item uses a stable key
(label is fine here) and preserve the existing className and onClick behavior.
- Around line 23-25: The local function handleClick simply calls openNotice()
and is unnecessary; remove the handleClick declaration and update the element
using it to pass the handler directly as onClick={openNotice} (replace any
references to handleClick with openNotice) so the component uses openNotice as
the click handler without the extra wrapper.

In `@src/app/not-found.tsx`:
- Around line 5-47: The NotFound component uses a missing CSS utility class
shimmer-text; add a definition for .shimmer-text in your global utilities or
components styles (e.g., under your utilities/styles module) that implements the
shimmer effect (background gradient, background-clip:text or color overlay,
keyframes for sliding shimmer, and animation properties) so the two <p> elements
in NotFound render with the intended animated shimmer; ensure you include the
keyframes under the same stylesheet and export/import the stylesheet so it is
applied globally.

In `@src/components/common/SpaceBackground.tsx`:
- Line 1: This component appears to be pure presentational (no hooks, state, or
event handlers) so remove the "use client" directive at the top of
SpaceBackground.tsx to allow it to be a server component and reduce client
bundle size; verify the SpaceBackground component (and any helpers it imports)
do not use React hooks, browser-only APIs, or event handlers, and if none are
present simply delete the `"use client"` line and keep the existing JSX/exports.

In `@src/components/common/WipFeatureCard.tsx`:
- Around line 14-17: The div in WipFeatureCard (the JSX element with
className={clsx("relative w-[min(440px,90vw)]", className)} and onClick={(e) =>
e.stopPropagation()}) currently has an onClick handler but no accessibility
role; update that element to include role="presentation" (and optionally
tabIndex={-1} if needed) to indicate it is non-interactive and silence
keyboard/accessibility warnings while preserving the stopPropagation behavior.
- Line 28: The class string in WipFeatureCard.tsx contains an undefined Tailwind
class "rotate-80", so the rotation won't work; replace "rotate-80" with a valid
Tailwind rotate utility (e.g., "rotate-90" or "rotate-45") in the className
where "transition rotate-80 " appears (inside the WipFeatureCard component) or
add a custom rotate value to tailwind.config.ts if a non-standard angle is
required; ensure the updated className uses a supported rotate-* utility so the
rotation animates as intended.

In `@src/components/common/WipFeatureOverlay.tsx`:
- Around line 12-15: The component WipFeatureOverlay returns null on server via
a typeof document === "undefined" check which causes hydration mismatch; replace
that check by tracking client mount with useState/useEffect (e.g., const
[mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []))
and only render the Portal when mounted is true (and isOpen as before), removing
the direct typeof document guard so server renders the same markup as the client
until the client mount toggles rendering.
- Around line 20-23: The backdrop div in WipFeatureOverlay (the element with
className "fixed inset-0 z-60 flex items-center justify-center" and
onClick={onClose}) lacks keyboard handling and ARIA attributes; add an onKeyDown
handler that listens for Escape and calls onClose, make the element focusable
with tabIndex={0}, and add appropriate ARIA attributes (e.g., role="button" or
role="presentation" depending on surrounding markup, and aria-label like "Close
overlay" or use role="dialog" + aria-modal="true" on the modal container if this
is the dialog root) so keyboard users can close the overlay. Ensure the onClose
prop is reused and that focus is moved into/out of the overlay as appropriate
(e.g., set initial focus on mount or return focus on unmount) to complete
accessible behavior.

In `@src/hooks/useWipFeatureNotice.ts`:
- Around line 15-23: The openNotice function can leave dangling timers; store
the timeout ID in a ref (e.g., noticeTimerRef) and before creating a new
window.setTimeout clear any existing timer with
clearTimeout(noticeTimerRef.current), assign the new ID to
noticeTimerRef.current, and ensure you add a useEffect cleanup that clears
noticeTimerRef.current on unmount to prevent multiple simultaneous timers and
setState calls after unmount (affecting openNotice, setIsOpen, and autoCloseMs).
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 116106b and 7ca740e.

⛔ Files ignored due to path filters (5)
  • public/error/404.svg is excluded by !**/*.svg
  • public/error/double-arrow-icon.svg is excluded by !**/*.svg
  • public/error/elephant.svg is excluded by !**/*.svg
  • public/error/oops.svg is excluded by !**/*.svg
  • public/error/wip-card.svg is excluded by !**/*.svg
📒 Files selected for processing (12)
  • src/app/(layout)/(shell)/_components/dock/DockMenu.tsx
  • src/app/(layout)/(shell)/_components/dock/DockMenuItem.tsx
  • src/app/(layout)/(shell)/_components/dock/dock.types.ts
  • src/app/(layout)/(shell)/_components/home/HeroRightMedia.tsx
  • src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx
  • src/app/globals.css
  • src/app/not-found.tsx
  • src/components/common/SpaceBackground.tsx
  • src/components/common/WipFeatureCard.tsx
  • src/components/common/WipFeatureOverlay.tsx
  • src/hooks/useWipFeatureNotice.ts
  • src/styles/components/background.css
🧰 Additional context used
🧬 Code graph analysis (5)
src/app/(layout)/(shell)/_components/dock/DockMenuItem.tsx (3)
src/app/(layout)/(shell)/_components/dock/dock.types.ts (1)
  • DockMenuItemProps (45-50)
src/app/(layout)/(shell)/_components/dock/_hooks/useDockMenu.ts (3)
  • e (21-36)
  • dockState (7-76)
  • setDockState (51-53)
src/app/(layout)/(shell)/_components/dock/_hooks/useDockInteraction.ts (1)
  • entry (35-39)
src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx (2)
src/hooks/useWipFeatureNotice.ts (1)
  • useWipFeatureNotice (10-34)
src/components/common/WipFeatureOverlay.tsx (1)
  • WipFeatureOverlay (12-51)
src/components/common/WipFeatureOverlay.tsx (1)
src/components/common/WipFeatureCard.tsx (1)
  • WipFeatureCard (12-44)
src/app/(layout)/(shell)/_components/dock/DockMenu.tsx (1)
src/hooks/useWipFeatureNotice.ts (1)
  • useWipFeatureNotice (10-34)
src/app/not-found.tsx (1)
src/components/common/SpaceBackground.tsx (1)
  • SpaceBackground (9-28)
🪛 Biome (2.1.2)
src/components/common/WipFeatureCard.tsx

[error] 14-17: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 14-17: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)

src/components/common/WipFeatureOverlay.tsx

[error] 20-23: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 20-23: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)

src/app/globals.css

[error] 23-23: This @import is in the wrong position.

Any @import rules must precede all other valid at-rules and style rules in a stylesheet (ignoring @charset and @layer), or else the @import rule is invalid.
Consider moving import position.

(lint/correctness/noInvalidPositionAtImportRule)

🔇 Additional comments (10)
src/app/(layout)/(shell)/_components/dock/dock.types.ts (1)

45-50: LGTM!

DockMenuItemPropsonClick 옵셔널 prop을 추가한 것은 적절합니다. 기존 item.onClick과 별개로 외부에서 클릭 동작을 오버라이드할 수 있는 유연성을 제공합니다.

src/app/globals.css (1)

22-23: LGTM! (정적 분석 경고 확인 필요)

배경 스타일 import 추가는 적절합니다. Biome에서 @import 위치 관련 경고가 발생했으나, 모든 @import가 스타일 규칙(.heading-anchor) 이전에 위치하므로 CSS 명세상 문제가 없어 보입니다.

만약 빌드나 스타일 적용에 문제가 발생한다면, 다른 @import들과 동일한 섹션(line 6-20 사이)으로 이동을 고려해 주세요.

src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx (1)

178-179: LGTM!

WipFeatureOverlaycreatePortal을 사용하여 document.body에 렌더링되므로, <header> 내부에 배치해도 문제없습니다. WIP 기능 통합이 잘 구현되었습니다.

src/components/common/SpaceBackground.tsx (1)

9-27: LGTM!

깔끔한 프레젠테이셔널 컴포넌트입니다. clsx를 활용한 className 병합과 레이어 구조가 잘 구성되어 있습니다.

src/app/(layout)/(shell)/_components/dock/DockMenu.tsx (1)

156-160: LGTM!

WipFeatureOverlay 통합이 SiteHeader.tsx와 일관된 패턴으로 잘 구현되었습니다.

src/styles/components/background.css (1)

1-74: LGTM! 우주 배경 스타일이 잘 구성되어 있습니다.

애니메이션과 그라디언트가 적절히 정의되어 있고, 별똥별 효과의 딜레이 분산도 자연스럽습니다. translate3d를 사용하여 GPU 가속을 활용한 점도 좋습니다.

src/app/(layout)/(shell)/_components/dock/DockMenuItem.tsx (2)

8-8: LGTM! onClick prop 추가가 적절합니다.

외부에서 클릭 핸들러를 주입할 수 있게 하면서, item.onClick으로의 폴백을 유지하여 기존 동작과 호환됩니다.


42-54: 버튼 타입에서의 onClick 처리가 잘 되어 있습니다.

널 병합 연산자(??)를 사용하여 외부 onClick이 없을 때 item.onClick으로 자연스럽게 폴백됩니다.

src/components/common/WipFeatureCard.tsx (1)

12-43: 전반적으로 잘 구현된 WIP 카드 컴포넌트입니다.

닫기 버튼에 적절한 aria-label이 있고, clsx를 사용한 클래스 병합과 stopPropagation을 통한 모달 패턴 구현이 적절합니다.

src/components/common/WipFeatureOverlay.tsx (1)

24-45: 애니메이션 로직 확인 완료

Framer Motion의 AnimatePresencemotion.div를 사용한 슬라이드 애니메이션 구현이 적절합니다. key prop이 있어 AnimatePresence가 exit 애니메이션을 올바르게 처리할 수 있습니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +90 to +93
const handleItemClick =
item.type === "button" && item.label === "PHOTOBOOTH"
? () => openNotice()
: undefined;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

매직 스트링 사용에 대한 우려

item.label === "PHOTOBOOTH" 문자열 비교는 라벨이 변경될 경우 조용히 실패할 수 있습니다. WIP 상태를 dock item 설정에 명시적으로 추가하는 것이 더 안전합니다.

♻️ 개선 제안

DOCK_ITEMS 설정에 isWip?: boolean 속성을 추가:

// dockItem.config.ts
{
  type: "button",
  label: "PHOTOBOOTH",
  icon: "camera",
  isWip: true, // WIP 상태 명시
  onClick: () => {},
}
// DockMenu.tsx
const handleItemClick =
-  item.type === "button" && item.label === "PHOTOBOOTH"
+  item.type === "button" && item.isWip
    ? () => openNotice()
    : undefined;
🤖 Prompt for AI Agents
In `@src/app/`(layout)/(shell)/_components/dock/DockMenu.tsx around lines 90 - 93,
Replace the fragile magic-string check in handleItemClick by introducing an
explicit isWip flag on the dock item config and using that flag for behavior:
add isWip?: boolean to DOCK_ITEMS entries (e.g., the PHOTOBOOTH button), update
the type for dock items to include isWip, and change the handleItemClick logic
to use item.isWip (combined with type/button check) to decide whether to call
openNotice instead of comparing item.label to "PHOTOBOOTH".

Comment on lines +8 to +15
<Image
src="/error/elephant.svg"
alt="임시"
width={40}
height={120}
className="w-full h-auto select-none drop-shadow-[10px_5px_3px_rgba(0,0,0,0.10)]"
priority={false}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find public -name "*.svg" -o -name "elephant.svg" -o -name "wip-card.svg" 2>/dev/null | head -20

Repository: swallowedB/b-log

Length of output: 332


🏁 Script executed:

cat -n src/app/\(layout\)/\(shell\)/_components/home/HeroRightMedia.tsx

Repository: swallowedB/b-log

Length of output: 578


🏁 Script executed:

git log --oneline -5 -- src/app/\(layout\)/\(shell\)/_components/home/HeroRightMedia.tsx 2>/dev/null || echo "No git history available"

Repository: swallowedB/b-log

Length of output: 89


🏁 Script executed:

git diff HEAD~1 HEAD -- public/ 2>/dev/null | grep -E "elephant|wip-card" || echo "No recent changes to public/ found"

Repository: swallowedB/b-log

Length of output: 94


alt 텍스트 개선이 필요합니다

이미지 경로와 파일은 정상적으로 존재하는데, 몇 가지 지점이 있습니다:

  1. alt 텍스트: "임시" 는 스크린 리더 사용자에게 의미 있는 정보를 제공하지 못합니다. 일러스트의 목적이나 내용을 설명하는 텍스트로 변경하거나, 순수 장식용이라면 alt="" 로 설정하세요.

  2. 이미지 선택: /error/elephant.svg/error/wip-card.svg 가 모두 존재하는데, 이 컴포넌트에서 elephant.svg를 의도적으로 사용하는 건지 확인이 필요합니다.

  3. priority={false}: Next.js Image의 기본값이므로 생략 가능합니다.

🤖 Prompt for AI Agents
In `@src/app/`(layout)/(shell)/_components/home/HeroRightMedia.tsx around lines 8
- 15, Update the Image in the HeroRightMedia component: replace the
non-descriptive alt "임시" with a meaningful description of the illustration
(e.g., describe the elephant illustration’s purpose) or set alt="" if the
graphic is purely decorative; verify whether the intended asset is
"/error/elephant.svg" or "/error/wip-card.svg" and switch the src to the correct
file if needed; and remove the explicit priority={false} prop since it’s the
Next.js default.

Comment on lines 5 to 47
export default function NotFound() {
return (
<section className="relative flex flex-col items-center justify-center min-h-screen w-full overflow-hidden px-6">
<SpaceBackground />

<div className="flex flex-col items-center gap-5 h-full mb-10">
<h1>
<Image
src="/error/404.svg"
alt="404"
width={300}
height={150}
priority
/>
</h1>

<Image src="/error/oops.svg" alt="oops" width={220} height={80} />

<Image
className="w-10 aspect-auto"
src="/error/double-arrow-icon.svg"
alt=""
width={40}
height={40}
/>
</div>

<p className="text-center font-sans text-white/75 shimmer-text font-medium leading-relaxed max-w-[420px]">
보아뱀이 잘못된 페이지를 삼켜버렸어요.
</p>
<p className="text-center font-sans text-sm text-white/55 shimmer-text font-medium leading-relaxed max-w-[420px]">
찾으려던 글은 아직 소화되지 않았거나, 존재하지 않는 페이지예요.
</p>

<Link
href="/"
className="mt-8 inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/5 px-5 py-2 text-sm font-medium text-white/90 backdrop-blur-md transition hover:bg-white/15 hover:border-white/40"
>
<span>홈으로 돌아가기</span>
</Link>
</section>
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# 프로젝트 구조 확인
git ls-files | head -20

Repository: swallowedB/b-log

Length of output: 522


🏁 Script executed:

# shimmer-text 클래스 검색
rg -n "shimmer-text" --type css --type scss

Repository: swallowedB/b-log

Length of output: 88


🏁 Script executed:

# globals 또는 global 스타일 파일 찾기
fd -e css -e scss | grep -i "global\|style"

Repository: swallowedB/b-log

Length of output: 333


🏁 Script executed:

# app 디렉토리 구조 확인
fd -t f . src/app -type f | head -30

Repository: swallowedB/b-log

Length of output: 230


🏁 Script executed:

# shimmer-text 클래스를 모든 CSS 파일에서 검색
rg "shimmer-text" src/ --type css

Repository: swallowedB/b-log

Length of output: 42


🏁 Script executed:

# not-found.tsx 파일 내용 확인
cat -n src/app/not-found.tsx

Repository: swallowedB/b-log

Length of output: 1878


🏁 Script executed:

# 루트 레이아웃 확인
cat -n src/app/layout.tsx | head -50

Repository: swallowedB/b-log

Length of output: 1614


🏁 Script executed:

# shimmer-text를 모든 파일에서 검색 (CSS가 아닌 다른 형식일 수도 있음)
rg "shimmer-text" src/

Repository: swallowedB/b-log

Length of output: 337


🏁 Script executed:

# globals.css 전체 내용 확인
cat -n src/app/globals.css

Repository: swallowedB/b-log

Length of output: 1584


🏁 Script executed:

# tailwind config 파일 확인
fd -e config.ts -e config.js | grep -i tailwind

Repository: swallowedB/b-log

Length of output: 78


🏁 Script executed:

# tailwind.config.ts 또는 tailwind.config.js 확인
cat -n tailwind.config.* 2>/dev/null || echo "No tailwind config found"

Repository: swallowedB/b-log

Length of output: 409


🏁 Script executed:

# 다른 CSS 파일들도 확인 - shimmer-text가 정의되어 있을 수 있음
cat -n src/styles/components/*.css

Repository: swallowedB/b-log

Length of output: 10898


🏁 Script executed:

# 나머지 CSS 파일 검색
cat -n src/styles/tokens.css

Repository: swallowedB/b-log

Length of output: 1012


🏁 Script executed:

# MDX 스타일 확인
cat -n src/styles/mdx.css

Repository: swallowedB/b-log

Length of output: 3515


🏁 Script executed:

# 유틸리티 CSS 파일들 확인
cat -n src/styles/utilities/*.css

Repository: swallowedB/b-log

Length of output: 3487


🏁 Script executed:

# 전체 repo에서 shimmer 검색
rg "shimmer" --type css

Repository: swallowedB/b-log

Length of output: 42


shimmer-text 클래스 정의가 누락되었습니다.

not-found.tsx 32, 35줄에서 shimmer-text 클래스를 사용하고 있지만, 글로벌 스타일이나 CSS 파일 어디에도 정의되지 않았습니다. 해당 클래스를 src/styles/utilities/ 또는 src/styles/components/에 추가하여 정의해야 합니다.

🤖 Prompt for AI Agents
In `@src/app/not-found.tsx` around lines 5 - 47, The NotFound component uses a
missing CSS utility class shimmer-text; add a definition for .shimmer-text in
your global utilities or components styles (e.g., under your utilities/styles
module) that implements the shimmer effect (background gradient,
background-clip:text or color overlay, keyframes for sliding shimmer, and
animation properties) so the two <p> elements in NotFound render with the
intended animated shimmer; ensure you include the keyframes under the same
stylesheet and export/import the stylesheet so it is applied globally.

Comment on lines +14 to +17
<div
className={clsx("relative w-[min(440px,90vw)]", className)}
onClick={(e) => e.stopPropagation()}
>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

이벤트 전파 방지용 onClick에 대한 접근성 고려

정적 분석에서 지적된 대로, divonClick이 있지만 키보드 이벤트가 없습니다. 이 경우 stopPropagation() 목적이므로 실제 인터랙션은 아니지만, 접근성 도구 경고를 해소하려면 role="presentation"을 추가할 수 있습니다.

♻️ 접근성 속성 추가 제안
     <div
       className={clsx("relative w-[min(440px,90vw)]", className)}
       onClick={(e) => e.stopPropagation()}
+      role="presentation"
     >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className={clsx("relative w-[min(440px,90vw)]", className)}
onClick={(e) => e.stopPropagation()}
>
<div
className={clsx("relative w-[min(440px,90vw)]", className)}
onClick={(e) => e.stopPropagation()}
role="presentation"
>
🧰 Tools
🪛 Biome (2.1.2)

[error] 14-17: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 14-17: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)

🤖 Prompt for AI Agents
In `@src/components/common/WipFeatureCard.tsx` around lines 14 - 17, The div in
WipFeatureCard (the JSX element with className={clsx("relative
w-[min(440px,90vw)]", className)} and onClick={(e) => e.stopPropagation()})
currently has an onClick handler but no accessibility role; update that element
to include role="presentation" (and optionally tabIndex={-1} if needed) to
indicate it is non-interactive and silence keyboard/accessibility warnings while
preserving the stopPropagation behavior.

"h-7 w-7 rounded-full",
"text-black/20 hover:text-white",
"hover:bg-black/10 cursor-pointer",
"transition rotate-80 "
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# rotate-80 클래스 정의 확인
echo "=== 1. CSS 파일에서 rotate-80 검색 ==="
rg -n "rotate-80" --type css 2>/dev/null || echo "No CSS matches found"

echo -e "\n=== 2. tailwind.config 파일에서 rotate 관련 설정 검색 ==="
fd "tailwind.config" -x cat {} \;

echo -e "\n=== 3. 전체 프로젝트에서 rotate-80 검색 ==="
rg -n "rotate-80" 2>/dev/null || echo "No matches found"

echo -e "\n=== 4. 프로젝트 구조 확인 ==="
ls -la | grep -E "tailwind|css"
find . -maxdepth 2 -name "*.config.*" -o -name "tailwind*" 2>/dev/null

Repository: swallowedB/b-log

Length of output: 820


🏁 Script executed:

#!/bin/bash
# WipFeatureCard.tsx에서 rotate-80이 사용된 컨텍스트 확인
sed -n '20,35p' src/components/common/WipFeatureCard.tsx

Repository: swallowedB/b-log

Length of output: 517


rotate-80은 정의되지 않은 클래스입니다. Tailwind CSS 기본 rotate 값(rotate-0, rotate-90, rotate-180 등) 중 하나로 수정이 필요합니다.

tailwind.config.ts와 프로젝트의 CSS 파일에서 rotate-80 정의를 찾을 수 없습니다. 현재 코드는 transition 효과는 적용되지만 회전 애니메이션은 작동하지 않습니다. 의도에 맞는 값(예: rotate-90)으로 수정해 주세요.

🤖 Prompt for AI Agents
In `@src/components/common/WipFeatureCard.tsx` at line 28, The class string in
WipFeatureCard.tsx contains an undefined Tailwind class "rotate-80", so the
rotation won't work; replace "rotate-80" with a valid Tailwind rotate utility
(e.g., "rotate-90" or "rotate-45") in the className where "transition rotate-80
" appears (inside the WipFeatureCard component) or add a custom rotate value to
tailwind.config.ts if a non-standard angle is required; ensure the updated
className uses a supported rotate-* utility so the rotation animates as
intended.

Comment on lines +12 to +15
export function WipFeatureOverlay({ isOpen, onClose }: WipFeatureOverlayProps) {
if (typeof document === "undefined") {
return null;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

하이드레이션 불일치 가능성

typeof document === "undefined" 체크는 SSR 에러를 방지하지만, 서버에서는 null을 반환하고 클라이언트에서는 Portal 콘텐츠를 반환하여 하이드레이션 불일치가 발생할 수 있습니다.

useEffect + useState를 사용하여 클라이언트 마운트 후에만 Portal을 렌더링하는 방식이 더 안전합니다.

♻️ 권장 수정안
+"use client";
+
+import { useEffect, useState } from "react";
 import { AnimatePresence, motion } from "framer-motion";
 import { createPortal } from "react-dom";
 import { WipFeatureCard } from "./WipFeatureCard";

 interface WipFeatureOverlayProps {
   isOpen: boolean;
   onClose: () => void;
 }

 export function WipFeatureOverlay({ isOpen, onClose }: WipFeatureOverlayProps) {
-  if (typeof document === "undefined") {
-    return null;
-  }
+  const [mounted, setMounted] = useState(false);
+
+  useEffect(() => {
+    setMounted(true);
+  }, []);
+
+  if (!mounted) {
+    return null;
+  }

   return createPortal(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function WipFeatureOverlay({ isOpen, onClose }: WipFeatureOverlayProps) {
if (typeof document === "undefined") {
return null;
}
"use client";
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { createPortal } from "react-dom";
import { WipFeatureCard } from "./WipFeatureCard";
interface WipFeatureOverlayProps {
isOpen: boolean;
onClose: () => void;
}
export function WipFeatureOverlay({ isOpen, onClose }: WipFeatureOverlayProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return createPortal(
// ... rest of component
);
}
🤖 Prompt for AI Agents
In `@src/components/common/WipFeatureOverlay.tsx` around lines 12 - 15, The
component WipFeatureOverlay returns null on server via a typeof document ===
"undefined" check which causes hydration mismatch; replace that check by
tracking client mount with useState/useEffect (e.g., const [mounted, setMounted]
= useState(false); useEffect(() => setMounted(true), [])) and only render the
Portal when mounted is true (and isOpen as before), removing the direct typeof
document guard so server renders the same markup as the client until the client
mount toggles rendering.

Comment on lines +20 to +23
<div
className="fixed inset-0 z-60 flex items-center justify-center"
onClick={onClose}
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

접근성 개선 필요 (키보드 이벤트 및 ARIA 속성)

정적 분석에서 지적한 대로, 백드롭 divonClick이 있지만 키보드 이벤트 핸들러가 없습니다. 모달/오버레이의 표준 UX로 Escape 키로 닫을 수 있어야 합니다.

🔧 접근성 개선 코드
+import { useEffect, useState, useCallback } from "react";
 export function WipFeatureOverlay({ isOpen, onClose }: WipFeatureOverlayProps) {
+  const handleKeyDown = useCallback(
+    (e: KeyboardEvent) => {
+      if (e.key === "Escape") {
+        onClose();
+      }
+    },
+    [onClose]
+  );
+
+  useEffect(() => {
+    if (isOpen) {
+      document.addEventListener("keydown", handleKeyDown);
+      return () => document.removeEventListener("keydown", handleKeyDown);
+    }
+  }, [isOpen, handleKeyDown]);

백드롭 div에 role과 aria 속성 추가:

         <div
-          className="fixed inset-0 z-60 flex items-center justify-center"
-          onClick={onClose} 
+          role="dialog"
+          aria-modal="true"
+          aria-label="준비중 기능 안내"
+          className="fixed inset-0 z-60 flex items-center justify-center bg-black/30"
+          onClick={onClose}
+          onKeyDown={(e) => e.key === "Escape" && onClose()}
         >
🧰 Tools
🪛 Biome (2.1.2)

[error] 20-23: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 20-23: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)

🤖 Prompt for AI Agents
In `@src/components/common/WipFeatureOverlay.tsx` around lines 20 - 23, The
backdrop div in WipFeatureOverlay (the element with className "fixed inset-0
z-60 flex items-center justify-center" and onClick={onClose}) lacks keyboard
handling and ARIA attributes; add an onKeyDown handler that listens for Escape
and calls onClose, make the element focusable with tabIndex={0}, and add
appropriate ARIA attributes (e.g., role="button" or role="presentation"
depending on surrounding markup, and aria-label like "Close overlay" or use
role="dialog" + aria-modal="true" on the modal container if this is the dialog
root) so keyboard users can close the overlay. Ensure the onClose prop is reused
and that focus is moved into/out of the overlay as appropriate (e.g., set
initial focus on mount or return focus on unmount) to complete accessible
behavior.

@swallowedB swallowedB merged commit fc876c3 into dev Jan 14, 2026
3 of 4 checks passed
@swallowedB swallowedB deleted the feat/404-48 branch January 14, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ feat: 글로벌 에러 / 404 페이지 구현

1 participant