diff --git a/public/tlqkf.webp b/public/tlqkf.webp new file mode 100644 index 0000000..3cd2962 Binary files /dev/null and b/public/tlqkf.webp differ diff --git a/public/txt.webp b/public/txt.webp new file mode 100644 index 0000000..8da4e9c Binary files /dev/null and b/public/txt.webp differ diff --git a/src/app/globals.css b/src/app/globals.css index b8898e1..f05c42f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,5 +1,6 @@ @import url("https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.3/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css"); @import url('https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&display=swap'); +@import url('https://cdn.jsdelivr.net/npm/galmuri@latest/dist/galmuri.css'); @import "tailwindcss"; @import "tw-animate-css"; @@ -14,6 +15,10 @@ font-family: "NType82Headline", sans-serif !important; } +.font-galmuri { + font-family: "Galmuri", sans-serif !important; +} + :root { --background: hsl(340 40% 98%); --foreground: hsl(315 21% 8%); diff --git a/src/app/page.tsx b/src/app/page.tsx index f67eb70..322ce83 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,13 +10,24 @@ import { Banner } from "@/components/ui/banner"; import { LinkIcon, TreeDeciduous, TreePalmIcon, TreesIcon } from "lucide-react"; import { useState } from "react"; import Contact from "@/components/Contact"; +import DraggableWindose from "@/components/DraggableWindose"; +import TXT from "@/components/txt"; export default function Page() { const { theme } = useTheme(); const [show, setShow] = useState(true); + const [targetPosition, setTargetPosition] = useState<{ x: number; y: number } | null>(null); + const [windowVisible, setWindowVisible] = useState(true); + + const handleTXTHover = (position: { x: number; y: number } | null) => { + setTargetPosition(position); + if (position) { + setWindowVisible(true); + } + }; return ( -
+
@@ -54,7 +65,8 @@ export default function Page() { />
- + setWindowVisible(false)} onDragStart={() => setTargetPosition(null)} /> +

나의 어두움이 다른 사람들에겐 이 되길 바라는 개발자, 정보보안전문가 남현석입니다.

초등학교 시절 운영체제에 흥미를 느껴 컴퓨터를 시작했고, 이후 프로그래밍에 관심을 갖게 되었습니다.

diff --git a/src/components/DraggableWindose.tsx b/src/components/DraggableWindose.tsx new file mode 100644 index 0000000..1bac1d5 --- /dev/null +++ b/src/components/DraggableWindose.tsx @@ -0,0 +1,141 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import Image from 'next/image'; +import { useIsMobile } from '@/hooks/use-mobile'; + +export default function DraggableWindose({ targetPosition, isVisible, onClose, onDragStart }: { targetPosition: { x: number; y: number } | null; isVisible: boolean; onClose: () => void; onDragStart?: () => void }) { + const isMobile = useIsMobile(); + const [position, setPosition] = useState({ x: 100, y: 100 }); + const [isDragging, setIsDragging] = useState(false); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const [shakeCount, setShakeCount] = useState(0); + const windowRef = useRef(null); + const lastPositionRef = useRef({ x: 100, y: 100 }); + const shakeThresholdRef = useRef(0); + + const handleMouseDown = (e: React.MouseEvent) => { + if (e.button !== 0) return; // Only left click + setIsDragging(true); + setShakeCount(0); + shakeThresholdRef.current = 0; + onDragStart?.(); + if (windowRef.current) { + const rect = windowRef.current.getBoundingClientRect(); + setDragOffset({ + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }); + } + }; + + useEffect(() => { + if (!isDragging) return; + + const handleMouseMove = (e: MouseEvent) => { + const newX = e.clientX - dragOffset.x; + const newY = e.clientY - dragOffset.y; + + setPosition({ + x: newX, + y: newY, + }); + + // 흔드는 감지 + const distance = Math.sqrt( + Math.pow(newX - lastPositionRef.current.x, 2) + + Math.pow(newY - lastPositionRef.current.y, 2) + ); + + lastPositionRef.current = { x: newX, y: newY }; + shakeThresholdRef.current += distance; + + if (shakeThresholdRef.current > 2000) { + setShakeCount(prev => prev + 1); + shakeThresholdRef.current = 0; + + if (shakeCount >= 2) { + onClose(); + setIsDragging(false); + } + } + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [isDragging, dragOffset, shakeCount]); + + useEffect(() => { + if (isDragging || targetPosition) return; + + const interval = setInterval(() => { + setPosition({ + x: Math.random() * (window.innerWidth - 500), + y: Math.random() * (window.innerHeight - 400), + }); + }, 500); + + return () => clearInterval(interval); + }, [isDragging, targetPosition]); + + useEffect(() => { + if (targetPosition && !isDragging) { + setPosition(targetPosition); + } + }, [targetPosition, isDragging]); + + return ( + isMobile ? ( +
+ + Banner + +
+ ) : ( + isVisible && ( +
+
+ Draggable Window +
+
+ ) + ) + ); +} diff --git a/src/components/txt.tsx b/src/components/txt.tsx new file mode 100644 index 0000000..453ff4e --- /dev/null +++ b/src/components/txt.tsx @@ -0,0 +1,22 @@ +import Image from "next/image"; + +interface TXTProps { + onHover?: (position: { x: number; y: number } | null) => void; +} + +export default function TXT({ onHover }: TXTProps) { + return ( +
onHover?.({ x: 16, y: 16 })} + onMouseLeave={() => onHover?.(null)} + onClick={() => { + alert("왜 편법을 쓰지?"); + window.location.reload(); + }} + > + txt + 비밀.txt +
+ ); +} \ No newline at end of file