diff --git a/public/error/404.svg b/public/error/404.svg new file mode 100644 index 0000000..d1b9548 --- /dev/null +++ b/public/error/404.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/error/double-arrow-icon.svg b/public/error/double-arrow-icon.svg new file mode 100644 index 0000000..f345a6d --- /dev/null +++ b/public/error/double-arrow-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/error/elephant.svg b/public/error/elephant.svg new file mode 100644 index 0000000..ec8f5fa --- /dev/null +++ b/public/error/elephant.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/error/oops.svg b/public/error/oops.svg new file mode 100644 index 0000000..e2dfb2e --- /dev/null +++ b/public/error/oops.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/error/wip-card.svg b/public/error/wip-card.svg new file mode 100644 index 0000000..f0734ef --- /dev/null +++ b/public/error/wip-card.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/(layout)/(shell)/_components/dock/DockMenu.tsx b/src/app/(layout)/(shell)/_components/dock/DockMenu.tsx index c8a755a..ff586c6 100644 --- a/src/app/(layout)/(shell)/_components/dock/DockMenu.tsx +++ b/src/app/(layout)/(shell)/_components/dock/DockMenu.tsx @@ -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 { @@ -43,6 +45,10 @@ export default function DockMenu() { hideDelay: 800, }); + const { isOpen, openNotice, closeNotice } = useWipFeatureNotice({ + autoCloseMs: 2500, + }); + if (isHidden) return null; return ( @@ -82,6 +88,11 @@ export default function DockMenu() { const style = iconStyles[index] ?? DEFAULT_STYLE; + const handleItemClick = + item.type === "button" && item.label === "PHOTOBOOTH" + ? () => openNotice() + : undefined; + return ( { itemRefs.current[index] = el; }} + onClick={handleItemClick} /> ); })} @@ -142,6 +154,8 @@ export default function DockMenu() { )} + + ); } diff --git a/src/app/(layout)/(shell)/_components/dock/DockMenuItem.tsx b/src/app/(layout)/(shell)/_components/dock/DockMenuItem.tsx index 7d99949..0e18621 100644 --- a/src/app/(layout)/(shell)/_components/dock/DockMenuItem.tsx +++ b/src/app/(layout)/(shell)/_components/dock/DockMenuItem.tsx @@ -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 = ( {item.label} @@ -44,7 +44,7 @@ export const DockMenuItem = ({ item, style, onRef }: DockMenuItemProps) => { + + ))} @@ -111,26 +119,27 @@ export default function SiteHeader() { > )} + + ); } diff --git a/src/app/globals.css b/src/app/globals.css index 9a65a39..2702816 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -19,6 +19,9 @@ /* mdx 스타일링 */ @import "../styles/mdx.css"; +/* 백그라운드 애니메이션 우주 배경 */ +@import "../styles/components/background.css"; + .heading-anchor { text-decoration: none; } diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..a0bdf11 --- /dev/null +++ b/src/app/not-found.tsx @@ -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 ( +
+ + +
+

+ 404 +

+ + oops + + +
+ +

+ 보아뱀이 잘못된 페이지를 삼켜버렸어요. +

+

+ 찾으려던 글은 아직 소화되지 않았거나, 존재하지 않는 페이지예요. +

+ + + 홈으로 돌아가기 + +
+ ); +} diff --git a/src/components/common/SpaceBackground.tsx b/src/components/common/SpaceBackground.tsx new file mode 100644 index 0000000..5a07ef8 --- /dev/null +++ b/src/components/common/SpaceBackground.tsx @@ -0,0 +1,28 @@ +"use client"; + +import clsx from "clsx"; + +interface SpaceBackgroundProps { + className?: string; +} + +export function SpaceBackground({ className }: SpaceBackgroundProps) { + return ( +
+ {/* 작은 별 배경 레이어 */} +
+ + {/* 별똥별들 */} +
+
+
+
+
+
+ ); +} diff --git a/src/components/common/WipFeatureCard.tsx b/src/components/common/WipFeatureCard.tsx new file mode 100644 index 0000000..4f90b4a --- /dev/null +++ b/src/components/common/WipFeatureCard.tsx @@ -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 ( +
e.stopPropagation()} + > + + + 준비 중 안내 카드 +
+ ); +} diff --git a/src/components/common/WipFeatureOverlay.tsx b/src/components/common/WipFeatureOverlay.tsx new file mode 100644 index 0000000..3e99feb --- /dev/null +++ b/src/components/common/WipFeatureOverlay.tsx @@ -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; + } + + return createPortal( + + {isOpen && ( +
+ + + +
+ )} +
, + document.body + ); +} diff --git a/src/hooks/useWipFeatureNotice.ts b/src/hooks/useWipFeatureNotice.ts new file mode 100644 index 0000000..17280d8 --- /dev/null +++ b/src/hooks/useWipFeatureNotice.ts @@ -0,0 +1,48 @@ +"use client"; + +import { useEffect, useRef, useState, useCallback } from "react"; + +interface UseWipFeatureNoticeOptions { + autoCloseMs?: number; +} + +export function useWipFeatureNotice(options?: UseWipFeatureNoticeOptions) { + const [isOpen, setIsOpen] = useState(false); + const timerRef = useRef(null); + + const autoCloseMs = options?.autoCloseMs; + + const clearTimer = useCallback(() => { + if (timerRef.current !== null) { + window.clearTimeout(timerRef.current); + timerRef.current = null; + } + }, []); + + const openNotice = useCallback(() => { + clearTimer(); + setIsOpen(true); + + if (autoCloseMs) { + timerRef.current = window.setTimeout(() => { + setIsOpen(false); + timerRef.current = null; + }, autoCloseMs); + } + }, [autoCloseMs, clearTimer]); + + const closeNotice = useCallback(() => { + clearTimer(); + setIsOpen(false); + }, [clearTimer]); + + useEffect(() => { + return () => clearTimer(); + }, [clearTimer]); + + return { + isOpen, + openNotice, + closeNotice, + }; +} diff --git a/src/styles/components/background.css b/src/styles/components/background.css new file mode 100644 index 0000000..c216e7b --- /dev/null +++ b/src/styles/components/background.css @@ -0,0 +1,74 @@ +.space-bg-gradient { + background: radial-gradient(circle at 50% 30%, rgba(79, 70, 229, 0.45), rgba(4, 7, 33, 1)); +} + +/* 별이 살짝 움직이는 느낌 */ +@keyframes spaceStarsDrift { + from { + transform: translate3d(0, 0, 0); + } + to { + transform: translate3d(-80px, 80px, 0); + } +} + +/* 작은 별 레이어 */ +.space-bg-stars { + position: absolute; + inset: 0; + opacity: 0.85; + background-image: + radial-gradient(1px 1px at 10px 20px, rgba(255, 255, 255, 0.7), transparent), + radial-gradient(1.5px 1.5px at 130px 80px, rgba(191, 219, 254, 0.8), transparent), + radial-gradient(1px 1px at 200px 150px, rgba(221, 214, 254, 0.7), transparent), + radial-gradient(1px 1px at 300px 40px, rgba(248, 250, 252, 0.7), transparent), + radial-gradient(1.5px 1.5px at 50px 200px, rgba(191, 219, 254, 0.9), transparent); + background-repeat: repeat; + background-size: 260px 260px; + animation: spaceStarsDrift 20s linear infinite; +} + +/* 별똥별 */ +@keyframes shootingStar { + 0% { + transform: translate3d(0, 0, 0) rotate(-50deg); + opacity: 0; + } + 10% { + opacity: 1; + } + 100% { + transform: translate3d(-800px, 800px, 0) rotate(-30deg); + opacity: 0; + } +} + + +.shooting-star { + position: absolute; + width: 120px; + height: 2px; + background: linear-gradient(90deg, rgba(248, 250, 252, 0.95), transparent); + border-radius: 9999px; + filter: drop-shadow(0 0 6px rgba(248, 250, 252, 0.8)); + animation: shootingStar 2.2s ease-out infinite; +} + +/* 개별 별똥별 위치/딜레이 조정 */ +.shooting-star--1 { + top: 8%; + right: -90px; + animation-delay: 0s; +} + +.shooting-star--2 { + top: 30%; + right: -190px; + animation-delay: 0.9s; +} + +.shooting-star--3 { + top: 55%; + right: -190px; + animation-delay: 1.6s; +}