Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions public/error/404.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/error/double-arrow-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions public/error/elephant.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/error/oops.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions public/error/wip-card.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 15 additions & 1 deletion src/app/(layout)/(shell)/_components/dock/DockMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
import { DOCK_ITEMS } from "@/app/(layout)/(shell)/_components/dock/_constants/dockItem.config";
import { useDockCloseHint } from "@/app/(layout)/(shell)/_components/dock/_hooks/useDockCloseHint";
import { useDockInteraction } from "@/app/(layout)/(shell)/_components/dock/_hooks/useDockInteraction";
import { WipFeatureOverlay } from "@/components/common/WipFeatureOverlay";
import { useWipFeatureNotice } from "@/hooks/useWipFeatureNotice";
import clsx from "clsx";
import { AnimatePresence, motion } from "framer-motion";
import { ChevronsDown } from "lucide-react";
import { DEFAULT_STYLE } from "./_constants/dock.config";
import { useDockMenu } from "./_hooks/useDockMenu";
import { getDockClasses } from "./dock.utils";
import { DockMenuItem } from "./DockMenuItem";
import { useDockMenu } from "./_hooks/useDockMenu";

export default function DockMenu() {
const {
Expand Down Expand Up @@ -43,6 +45,10 @@ export default function DockMenu() {
hideDelay: 800,
});

const { isOpen, openNotice, closeNotice } = useWipFeatureNotice({
autoCloseMs: 2500,
});

if (isHidden) return null;

return (
Expand Down Expand Up @@ -82,6 +88,11 @@ export default function DockMenu() {

const style = iconStyles[index] ?? DEFAULT_STYLE;

const handleItemClick =
item.type === "button" && item.label === "PHOTOBOOTH"
? () => openNotice()
: undefined;
Comment on lines +91 to +94
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".


return (
<DockMenuItem
key={item.label}
Expand All @@ -90,6 +101,7 @@ export default function DockMenu() {
onRef={(el) => {
itemRefs.current[index] = el;
}}
onClick={handleItemClick}
/>
);
})}
Expand Down Expand Up @@ -142,6 +154,8 @@ export default function DockMenu() {
</motion.button>
)}
</AnimatePresence>

<WipFeatureOverlay isOpen={isOpen} onClose={closeNotice} />
</div>
);
}
4 changes: 2 additions & 2 deletions src/app/(layout)/(shell)/_components/dock/DockMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Link from "next/link";
import { DOCK_CONFIG } from "./_constants/dock.config";
import { DockMenuItemProps } from "./dock.types";

export const DockMenuItem = ({ item, style, onRef }: DockMenuItemProps) => {
export const DockMenuItem = ({ item, style, onRef, onClick, }: DockMenuItemProps) => {
const renderContent = () => {
const imgElement = (
<img src={item.icon} alt={item.label} className="w-12 h-auto" />
Expand Down Expand Up @@ -44,7 +44,7 @@ export const DockMenuItem = ({ item, style, onRef }: DockMenuItemProps) => {
<button
type="button"
aria-label={item.label}
onClick={item.onClick}
onClick={onClick ?? item.onClick}
className={commonClasses}
tabIndex={0}
>
Expand Down
1 change: 1 addition & 0 deletions src/app/(layout)/(shell)/_components/dock/dock.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ export interface DockMenuItemProps {
item: Exclude<DockItem, DockDividerItem>;
style: IconStyle;
onRef: (el: HTMLLIElement | null) => void;
onClick?: () => void;
}
18 changes: 11 additions & 7 deletions src/app/(layout)/(shell)/_components/home/HeroRightMedia.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"use client";

import Image from "next/image";

export default function HeroRightMedia() {
return (
<div className="flex-1 hidden md:block">
<div className="relative mx-auto aspect-4/3 bg-foreground/20 max-w-lg w-full overflow-hidden rounded-3xl border">
{/* 배경 효과 (리퀴드 글래스, 글로우 등은 여기서 스타일링) */}
<div className="absolute inset-0" aria-hidden />
<div className="relative h-full w-full">
</div>
</div>
<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}
/>
Comment on lines +8 to +15
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.

</div>
)
);
}
59 changes: 34 additions & 25 deletions src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";
import { useSearchTransition } from "@/app/(layout)/(shell)/_components/home/_hooks/useSearchTransition";
import { WipFeatureOverlay } from "@/components/common/WipFeatureOverlay";
import { useWipFeatureNotice } from "@/hooks/useWipFeatureNotice";
import clsx from "clsx";
import { AnimatePresence, motion } from "framer-motion";
import { Menu, Search, X } from "lucide-react";
Expand All @@ -15,6 +17,10 @@ export default function SiteHeader() {
closeMobileMenu,
handleSearchClick,
} = useSearchTransition();
const WIP_NAV_ITEMS = ["Resume", "Guestbook", "Lab"] as const;
const { isOpen, openNotice, closeNotice } = useWipFeatureNotice({
autoCloseMs: 2500,
});

return (
<header className="fixed inset-x-0 top-0 z-40 border-b border-foreground/10 bg-background/5 backdrop-blur-md">
Expand Down Expand Up @@ -56,17 +62,19 @@ export default function SiteHeader() {

<div className="flex items-center gap-6">
<div className="hidden items-center gap-9 font-medium text-foreground/70 md:flex">
<nav aria-label="주요 페이지" >
<nav aria-label="주요 페이지">
<ul className="flex items-center gap-10">
<li className="hover:text-foreground">
<Link href="/resume">Resume</Link>
</li>
<li className="hover:text-foreground">
<Link href="/guestbook">Guestbook</Link>
</li>
<li className="hover:text-foreground">
<Link href="/lab">Lab</Link>
</li>
{WIP_NAV_ITEMS.map((label) => (
<li key={label} className="hover:text-foreground">
<button
type="button"
onClick={openNotice}
className="text-sm cursor-pointer"
>
{label}
</button>
</li>
))}
</ul>
</nav>

Expand Down Expand Up @@ -111,26 +119,27 @@ export default function SiteHeader() {
>
<nav className="px-10 pb-4 pt-3 text-lg font-medium text-foreground">
<ul className="flex flex-col gap-4">
<li>
<Link href="/resume" onClick={closeMobileMenu}>
Resume
</Link>
</li>
<li>
<Link href="/guestbook" onClick={closeMobileMenu}>
Guestbook
</Link>
</li>
<li>
<Link href="/lab" onClick={closeMobileMenu}>
Lab
</Link>
</li>
{WIP_NAV_ITEMS.map((label) => (
<li key={label} className="hover:text-foreground">
<button
type="button"
onClick={() => {
openNotice();
closeMobileMenu();
}}
className="text-sm cursor-pointer"
>
{label}
</button>
</li>
))}
</ul>
</nav>
</motion.div>
)}
</AnimatePresence>

<WipFeatureOverlay isOpen={isOpen} onClose={closeNotice} />
</header>
);
}
3 changes: 3 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
/* mdx 스타일링 */
@import "../styles/mdx.css";

/* 백그라운드 애니메이션 우주 배경 */
@import "../styles/components/background.css";

.heading-anchor {
text-decoration: none;
}
Expand Down
47 changes: 47 additions & 0 deletions src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { SpaceBackground } from "@/components/common/SpaceBackground";
import Image from "next/image";
import Link from "next/link";

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 font-medium leading-relaxed max-w-[420px]">
보아뱀이 잘못된 페이지를 삼켜버렸어요.
</p>
<p className="text-center font-sans text-sm text-white/55 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>
);
}
Comment on lines 5 to 47
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.

28 changes: 28 additions & 0 deletions src/components/common/SpaceBackground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

"use client" 필요성 확인

이 컴포넌트는 hooks, state, event handlers를 사용하지 않는 순수 프레젠테이셔널 컴포넌트입니다. CSS 애니메이션은 서버 컴포넌트에서도 동작하므로, "use client"가 반드시 필요하지 않을 수 있습니다.

현재 구현에 문제는 없지만, 서버 컴포넌트로 변환하면 번들 사이즈를 줄일 수 있습니다.

🤖 Prompt for AI Agents
In `@src/components/common/SpaceBackground.tsx` at 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.


import clsx from "clsx";

interface SpaceBackgroundProps {
className?: string;
}

export function SpaceBackground({ className }: SpaceBackgroundProps) {
return (
<div
className={clsx(
"absolute inset-0 -z-20 overflow-hidden space-bg-gradient",
className
)}
>
{/* 작은 별 배경 레이어 */}
<div className="space-bg-stars" />

{/* 별똥별들 */}
<div className="pointer-events-none absolute inset-0 -z-10">
<div className="shooting-star shooting-star--1" />
<div className="shooting-star shooting-star--2" />
<div className="shooting-star shooting-star--3" />
</div>
</div>
);
}
44 changes: 44 additions & 0 deletions src/components/common/WipFeatureCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import clsx from "clsx";
import { X } from "lucide-react";
import Image from "next/image";

interface WipFeatureCardProps {
onClose: () => void;
className?: string;
}

export function WipFeatureCard({ onClose, className }: WipFeatureCardProps) {
return (
<div
className={clsx("relative w-[min(440px,90vw)]", className)}
onClick={(e) => e.stopPropagation()}
>
Comment on lines +14 to +17
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.

<button
type="button"
onClick={onClose}
aria-label="준비중 안내 닫기"
className={clsx(
"absolute right-8 top-2 z-10 ",
"inline-flex items-center justify-center",
"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.

)}
>
<X size={16} strokeWidth={3} />
</button>

<Image
src="/error/wip-card.svg"
alt="준비 중 안내 카드"
width={740}
height={420}
className="w-full h-auto select-none drop-shadow-[10px_5px_3px_rgba(0,0,0,0.10)]"
priority={false}
/>
</div>
);
}
51 changes: 51 additions & 0 deletions src/components/common/WipFeatureOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

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;
}
Comment on lines +12 to +15
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.


return createPortal(
<AnimatePresence>
{isOpen && (
<div
className="fixed inset-0 z-60 flex items-center justify-center"
onClick={onClose}
>
Comment on lines +20 to +23
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.

<motion.div
key="wip-notice"
initial={{ opacity: 0, x: "100%" }}
animate={{
opacity: 1,
x: "0%",
transition: {
duration: 0.4,
ease: [0.22, 1, 0.36, 1],
},
}}
exit={{
opacity: 0,
x: "-100%",
transition: {
duration: 0.35,
ease: [0.45, 0, 0.55, 1],
},
}}
>
<WipFeatureCard onClose={onClose} />
</motion.div>
</div>
)}
</AnimatePresence>,
document.body
);
}
Loading
Loading