From 5f8a7ad9436b639b27107fe9d41d68353c8c6ee8 Mon Sep 17 00:00:00 2001 From: choiboa Date: Wed, 14 Jan 2026 20:54:15 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=20nofound=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/error/404.svg | 5 ++ public/error/double-arrow-icon.svg | 3 + public/error/oops.svg | 3 + src/app/globals.css | 3 + src/app/not-found.tsx | 47 ++++++++++++++ src/components/common/SpaceBackground.tsx | 28 +++++++++ src/styles/components/background.css | 74 +++++++++++++++++++++++ 7 files changed, 163 insertions(+) create mode 100644 public/error/404.svg create mode 100644 public/error/double-arrow-icon.svg create mode 100644 public/error/oops.svg create mode 100644 src/app/not-found.tsx create mode 100644 src/components/common/SpaceBackground.tsx create mode 100644 src/styles/components/background.css 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/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/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..e2da67a --- /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/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; +} From ad54d771f29c5cb0cfa195914c0f0534535d2e1e Mon Sep 17 00:00:00 2001 From: choiboa Date: Wed, 14 Jan 2026 23:56:43 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=20wipCard=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EC=A4=80=EB=B9=84=EC=A4=91=20=EC=95=88=EB=82=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/error/wip-card.svg | 43 ++++++++++++ .../(shell)/_components/dock/DockMenu.tsx | 18 ++++- .../(shell)/_components/dock/DockMenuItem.tsx | 4 +- .../(shell)/_components/dock/dock.types.ts | 1 + .../(shell)/_components/layout/SiteHeader.tsx | 68 ++++++++++++++++--- src/components/common/WipFeatureCard.tsx | 44 ++++++++++++ src/components/common/WipFeatureOverlay.tsx | 51 ++++++++++++++ src/hooks/useWipFeatureNotice.ts | 34 ++++++++++ 8 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 public/error/wip-card.svg create mode 100644 src/components/common/WipFeatureCard.tsx create mode 100644 src/components/common/WipFeatureOverlay.tsx create mode 100644 src/hooks/useWipFeatureNotice.ts 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..97d4959 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,9 @@ export default function DockMenu() { hideDelay: 800, }); + const { isOpen, openNotice, closeNotice } = + useWipFeatureNotice(); + if (isHidden) return null; return ( @@ -82,6 +87,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 +153,11 @@ 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) => {
  • - Guestbook +
  • -
  • - Lab +
  • +
  • @@ -112,25 +138,45 @@ export default function SiteHeader() { )} + + ); } 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..2114066 --- /dev/null +++ b/src/hooks/useWipFeatureNotice.ts @@ -0,0 +1,34 @@ +"use client"; + +import { useState } from "react"; + +interface UseWipFeatureNoticeOptions { + autoCloseMs?: number; +} + + +export function useWipFeatureNotice(options?: UseWipFeatureNoticeOptions) { + const [isOpen, setIsOpen] = useState(false); + + const autoCloseMs = options?.autoCloseMs; + + const openNotice = () => { + setIsOpen(true); + + if (autoCloseMs) { + window.setTimeout(() => { + setIsOpen(false); + }, autoCloseMs); + } + }; + + const closeNotice = () => { + setIsOpen(false); + }; + + return { + isOpen, + openNotice, + closeNotice, + }; +} From 7ca740ed1673d2e10e764889da9289f0fec3c3bc Mon Sep 17 00:00:00 2001 From: choiboa Date: Thu, 15 Jan 2026 00:08:59 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=92=84=20Style=20:=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=BC=EB=9F=AC=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/error/elephant.svg | 38 +++++++++++++++++++ .../_components/home/HeroRightMedia.tsx | 18 +++++---- 2 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 public/error/elephant.svg 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/src/app/(layout)/(shell)/_components/home/HeroRightMedia.tsx b/src/app/(layout)/(shell)/_components/home/HeroRightMedia.tsx index 4674594..7373c07 100644 --- a/src/app/(layout)/(shell)/_components/home/HeroRightMedia.tsx +++ b/src/app/(layout)/(shell)/_components/home/HeroRightMedia.tsx @@ -1,14 +1,18 @@ "use client"; +import Image from "next/image"; + export default function HeroRightMedia() { return (
    -
    - {/* 배경 효과 (리퀴드 글래스, 글로우 등은 여기서 스타일링) */} -
    -
    -
    -
    + 임시
    - ) + ); } From 30a046c8080c36c2e1550decfa89644133e90469 Mon Sep 17 00:00:00 2001 From: choiboa Date: Thu, 15 Jan 2026 00:29:09 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=A8=20Refactor=20:=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=EB=90=9C=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=ED=81=B4=EB=A6=B0=EC=97=85=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(shell)/_components/dock/DockMenu.tsx | 10 +- .../(shell)/_components/layout/SiteHeader.tsx | 95 ++++++------------- src/app/not-found.tsx | 4 +- src/hooks/useWipFeatureNotice.ts | 28 ++++-- 4 files changed, 56 insertions(+), 81 deletions(-) diff --git a/src/app/(layout)/(shell)/_components/dock/DockMenu.tsx b/src/app/(layout)/(shell)/_components/dock/DockMenu.tsx index 97d4959..ff586c6 100644 --- a/src/app/(layout)/(shell)/_components/dock/DockMenu.tsx +++ b/src/app/(layout)/(shell)/_components/dock/DockMenu.tsx @@ -45,8 +45,9 @@ export default function DockMenu() { hideDelay: 800, }); - const { isOpen, openNotice, closeNotice } = - useWipFeatureNotice(); + const { isOpen, openNotice, closeNotice } = useWipFeatureNotice({ + autoCloseMs: 2500, + }); if (isHidden) return null; @@ -154,10 +155,7 @@ export default function DockMenu() { )} - +
    ); } diff --git a/src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx b/src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx index 66ddb23..e1c8474 100644 --- a/src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx +++ b/src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx @@ -17,12 +17,10 @@ export default function SiteHeader() { closeMobileMenu, handleSearchClick, } = useSearchTransition(); - - const { isOpen, openNotice, closeNotice } = useWipFeatureNotice(); - - const handleClick = () => { - openNotice(); - }; + const WIP_NAV_ITEMS = ["Resume", "Guestbook", "Lab"] as const; + const { isOpen, openNotice, closeNotice } = useWipFeatureNotice({ + autoCloseMs: 2500, + }); return (
    @@ -66,33 +64,17 @@ export default function SiteHeader() {
    @@ -137,39 +119,20 @@ export default function SiteHeader() { > diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index e2da67a..a0bdf11 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -29,10 +29,10 @@ export default function NotFound() { />
    -

    +

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

    -

    +

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

    diff --git a/src/hooks/useWipFeatureNotice.ts b/src/hooks/useWipFeatureNotice.ts index 2114066..17280d8 100644 --- a/src/hooks/useWipFeatureNotice.ts +++ b/src/hooks/useWipFeatureNotice.ts @@ -1,30 +1,44 @@ "use client"; -import { useState } from "react"; +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 openNotice = () => { + const clearTimer = useCallback(() => { + if (timerRef.current !== null) { + window.clearTimeout(timerRef.current); + timerRef.current = null; + } + }, []); + + const openNotice = useCallback(() => { + clearTimer(); setIsOpen(true); if (autoCloseMs) { - window.setTimeout(() => { + timerRef.current = window.setTimeout(() => { setIsOpen(false); + timerRef.current = null; }, autoCloseMs); } - }; + }, [autoCloseMs, clearTimer]); - const closeNotice = () => { + const closeNotice = useCallback(() => { + clearTimer(); setIsOpen(false); - }; + }, [clearTimer]); + + useEffect(() => { + return () => clearTimer(); + }, [clearTimer]); return { isOpen,