FIX(app): fix navbar error

- Fix navigation bar issue
- Correct component rendering
This commit is contained in:
2025-11-30 23:05:32 +09:00
parent c6c065ef77
commit cd6dbf5c14
8 changed files with 94 additions and 781 deletions

View File

@@ -13,7 +13,7 @@ commonLabels:
# 이미지 태그 설정
images:
- name: ghcr.io/mayne0213/portfolio
newTag: main-sha-ab364cf43229f665303531dfae03af39df6fb2b1
newTag: main-sha-88c70df51ff99151aeec57bb855d36141c85bfe4
patchesStrategicMerge:
- deployment-patch.yaml

View File

@@ -271,4 +271,15 @@
.dark .stars {
opacity: 0.8;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -1,226 +0,0 @@
import SectionHeader from '@/components/landing/section-header';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { ExternalLink, BookOpen, BarChart3, Github } from 'lucide-react';
import Image, { StaticImageData } from 'next/image';
import Link from 'next/link';
import joossamHome from '@/public/joossam/home.png';
import jotionHome from '@/public/jotion/home.png';
import joossamMain from '@/public/joossam/main.png';
import jaejadleHome from '@/public/jaejadle/home.png';
import portfolioHome from '@/public/portfolio/home.png';
import todoListHome from '@/public/todoList/home.png';
import joviesHome from '@/public/jovies/home.png';
interface ProjectData {
title: string;
description: string;
longDescription: string;
tags: string[];
imageSrc: StaticImageData;
githubUrl?: string;
liveUrl?: string;
docsUrl?: string;
monitoringUrl?: string;
featured?: boolean;
}
const PROJECTS_DATA: ProjectData[] = [
{
title: 'Joossam English',
description: 'English learning platform for Korean students',
longDescription: 'A comprehensive English learning platform featuring interactive lessons, practice exercises, and progress tracking. Built with Next.js for optimal performance and SEO.',
tags: ['Next.js', 'TypeScript', 'Prisma', 'MySQL', 'NextAuth.js', 'Tailwind CSS'],
imageSrc: joossamHome,
githubUrl: 'https://github.com/minjo-on/joossam',
liveUrl: 'https://joossam.com',
docsUrl: '#',
monitoringUrl: '#',
featured: true,
},
{
title: 'Jotion',
description: 'Notion-like note-taking application',
longDescription: 'A powerful note-taking and knowledge management application inspired by Notion. Features rich text editing, hierarchical organization, and real-time collaboration.',
tags: ['Next.js', 'React', 'Convex', 'Clerk', 'BlockNote', 'Tailwind CSS'],
imageSrc: jotionHome,
githubUrl: 'https://github.com/minjo-on/jotion',
liveUrl: 'https://jotion.minjo.xyz',
docsUrl: '#',
monitoringUrl: '#',
featured: true,
},
{
title: 'Youni Classic',
description: 'Church community management system',
longDescription: 'A comprehensive church management system with member directory, event scheduling, and communication tools. Designed for efficient church administration.',
tags: ['Next.js', 'TypeScript', 'MySQL', 'Prisma', 'Tailwind CSS'],
imageSrc: joossamMain,
githubUrl: 'https://github.com/minjo-on/youniClassic',
docsUrl: '#',
monitoringUrl: '#',
featured: true,
},
{
title: 'Jaejadle Church',
description: 'Church website with event management',
longDescription: 'Modern church website featuring event calendars, sermon archives, and member portals. Built with focus on accessibility and mobile responsiveness.',
tags: ['Next.js', 'TypeScript', 'Tailwind CSS', 'Shadcn/ui'],
imageSrc: jaejadleHome,
githubUrl: 'https://github.com/minjo-on/jaejadle',
liveUrl: 'https://jaejadle.com',
docsUrl: '#',
monitoringUrl: '#',
},
{
title: 'Portfolio',
description: 'Personal portfolio website with Kubernetes deployment',
longDescription: 'My personal portfolio website showcasing projects and skills. Deployed on self-managed Kubernetes cluster with GitOps workflow using ArgoCD.',
tags: ['Next.js', 'TypeScript', 'Tailwind CSS', 'Docker', 'Kubernetes', 'ArgoCD'],
imageSrc: portfolioHome,
githubUrl: 'https://github.com/minjo-on/portfolio',
liveUrl: 'https://minjo.xyz',
docsUrl: '#',
monitoringUrl: '#',
},
{
title: '[Seminar] Todo List',
description: 'Educational todo list application for teaching',
longDescription: 'A todo list application created for teaching full-stack development concepts. Covers React fundamentals, state management, and API integration.',
tags: ['React', 'TypeScript', 'Vite', 'Tailwind CSS'],
imageSrc: todoListHome,
githubUrl: 'https://github.com/minjo-on/todoList',
docsUrl: '#',
monitoringUrl: '#',
},
{
title: 'Jovies',
description: 'Movie discovery and tracking application',
longDescription: 'A movie discovery platform integrating with TMDB API. Features movie search, ratings, reviews, and personal watchlists.',
tags: ['Next.js', 'TypeScript', 'TMDB API', 'Tailwind CSS'],
imageSrc: joviesHome,
githubUrl: 'https://github.com/minjo-on/jovies',
liveUrl: 'https://jovies.minjo.xyz',
docsUrl: '#',
monitoringUrl: '#',
},
];
function ProjectCard({
title,
description,
longDescription,
tags,
imageSrc,
githubUrl,
liveUrl,
docsUrl,
monitoringUrl,
featured,
}: ProjectData) {
return (
<Card className="overflow-hidden w-full p-0 hover:shadow-lg transition-shadow">
{/* Image Section */}
<div className="aspect-1440/770 relative bg-white">
<Image
src={imageSrc}
alt={title}
fill
className="object-cover border-b-2 border-gray-200"
/>
{featured && (
<div className="absolute top-4 right-4 bg-primary text-primary-foreground px-3 py-1 rounded-full text-sm font-semibold">
Featured
</div>
)}
</div>
{/* Content Section */}
<div className="px-6 pt-6 pb-4 flex flex-col gap-4">
<div className="flex flex-col gap-2">
<h3 className="font-semibold text-2xl">{title}</h3>
<p className="text-muted-foreground">{description}</p>
<p className="text-sm text-muted-foreground">{longDescription}</p>
</div>
{/* Tags */}
<div className="flex gap-2 flex-wrap">
{tags.map((tag) => (
<Button key={tag} variant="outline" size="sm" className="text-xs">
{tag}
</Button>
))}
</div>
</div>
{/* Links Section */}
<div className="px-6">
<Separator />
</div>
<div className="px-6 py-4 flex gap-4">
{githubUrl && (
<Link
href={githubUrl}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="GitHub"
>
<Github className="w-5 h-5" />
</Link>
)}
{liveUrl && (
<Link
href={liveUrl}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Live Demo"
>
<ExternalLink className="w-5 h-5" />
</Link>
)}
{docsUrl && (
<Link
href={docsUrl}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Documentation"
>
<BookOpen className="w-5 h-5" />
</Link>
)}
{monitoringUrl && (
<Link
href={monitoringUrl}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Monitoring"
>
<BarChart3 className="w-5 h-5" />
</Link>
)}
</div>
</Card>
);
}
export default function ProjectsPage() {
return (
<main className="page-container">
<SectionHeader
title="Projects"
description="A collection of my web development projects, from full-stack applications to educational demonstrations"
/>
<section className="grid grid-cols-1 pc:grid-cols-2 gap-6 pc:gap-8 max-w-[1400px]">
{PROJECTS_DATA.map((project, index) => (
<ProjectCard key={index} {...project} />
))}
</section>
</main>
);
}

View File

@@ -1,51 +0,0 @@
"use client";
import { cn } from "@/lib/utils";
import { useState } from "react";
interface MeteorsProps {
number?: number;
minDelay?: number;
maxDelay?: number;
minDuration?: number;
maxDuration?: number;
angle?: number;
className?: string;
}
export const Meteors = ({
number = 20,
minDelay = 0.2,
maxDelay = 1.2,
minDuration = 2,
maxDuration = 10,
angle = 215,
className,
}: MeteorsProps) => {
const [meteorStyles] = useState<Array<React.CSSProperties>>(() => {
return [...new Array(number)].map(() => ({
"--angle": -angle + "deg",
top: "-5%",
left: `calc(0% + ${Math.floor(Math.random() * (typeof window !== "undefined" ? window.innerWidth : 1000))}px)`,
animationDelay: Math.random() * (maxDelay - minDelay) + minDelay + "s",
animationDuration:
Math.floor(Math.random() * (maxDuration - minDuration) + minDuration) +
"s",
}));
});
return (
<div className="absolute inset-0 overflow-hidden">
{[...meteorStyles].map((style, idx) => (
// Meteor Head
<span
key={idx}
style={{ ...style }}
className={cn(
"pointer-events-none absolute size-1 rotate-(--angle) animate-meteor rounded-full bg-white shadow-[0_0_10px_2px_rgba(255,255,255,0.5)]",
className,
)}
>
{/* Meteor Tail */}
<div className="pointer-events-none absolute top-1/2 -z-10 h-px w-[100px] -translate-y-1/2 bg-linear-to-r from-white to-transparent" />
</span>
))}
</div>
);
};

View File

@@ -1,146 +0,0 @@
"use client";
import { cn } from "@/lib/utils";
import React, { useEffect, useState, useRef } from "react";
interface ShootingStar {
id: number;
x: number;
y: number;
angle: number;
scale: number;
speed: number;
distance: number;
}
interface ShootingStarsProps {
minSpeed?: number;
maxSpeed?: number;
minDelay?: number;
maxDelay?: number;
starColor?: string;
trailColor?: string;
starWidth?: number;
starHeight?: number;
className?: string;
}
const getRandomStartPoint = () => {
const side = Math.floor(Math.random() * 4);
const offset = Math.random() * window.innerWidth;
switch (side) {
case 0:
return { x: offset, y: 0, angle: 45 };
case 1:
return { x: window.innerWidth, y: offset, angle: 135 };
case 2:
return { x: offset, y: window.innerHeight, angle: 225 };
case 3:
return { x: 0, y: offset, angle: 315 };
default:
return { x: 0, y: 0, angle: 45 };
}
};
export const ShootingStars: React.FC<ShootingStarsProps> = ({
minSpeed = 10,
maxSpeed = 30,
minDelay = 1200,
maxDelay = 4200,
starColor = "#9E00FF",
trailColor = "#2EB9DF",
starWidth = 10,
starHeight = 1,
className,
}) => {
const [star, setStar] = useState<ShootingStar | null>(null);
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
const createStar = () => {
const { x, y, angle } = getRandomStartPoint();
const newStar: ShootingStar = {
id: Date.now(),
x,
y,
angle,
scale: 1,
speed: Math.random() * (maxSpeed - minSpeed) + minSpeed,
distance: 0,
};
setStar(newStar);
const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay;
setTimeout(createStar, randomDelay);
};
createStar();
return () => {};
}, [minSpeed, maxSpeed, minDelay, maxDelay]);
useEffect(() => {
const moveStar = () => {
if (star) {
setStar((prevStar) => {
if (!prevStar) return null;
const newX =
prevStar.x +
prevStar.speed * Math.cos((prevStar.angle * Math.PI) / 180);
const newY =
prevStar.y +
prevStar.speed * Math.sin((prevStar.angle * Math.PI) / 180);
const newDistance = prevStar.distance + prevStar.speed;
const newScale = 1 + newDistance / 100;
if (
newX < -20 ||
newX > window.innerWidth + 20 ||
newY < -20 ||
newY > window.innerHeight + 20
) {
return null;
}
return {
...prevStar,
x: newX,
y: newY,
distance: newDistance,
scale: newScale,
};
});
}
};
const animationFrame = requestAnimationFrame(moveStar);
return () => cancelAnimationFrame(animationFrame);
}, [star]);
return (
<svg
ref={svgRef}
className={cn("w-full h-full absolute inset-0", className)}
>
{star && (
<rect
key={star.id}
x={star.x}
y={star.y}
width={starWidth * star.scale}
height={starHeight}
fill="url(#gradient)"
transform={`rotate(${star.angle}, ${
star.x + (starWidth * star.scale) / 2
}, ${star.y + starHeight / 2})`}
/>
)}
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style={{ stopColor: trailColor, stopOpacity: 0 }} />
<stop
offset="100%"
style={{ stopColor: starColor, stopOpacity: 1 }}
/>
</linearGradient>
</defs>
</svg>
);
};

View File

@@ -1,234 +0,0 @@
"use client";
import React, { useEffect, useRef } from 'react';
import { Renderer, Program, Mesh, Triangle, Color } from 'ogl';
interface ThreadsProps {
color?: [number, number, number];
amplitude?: number;
distance?: number;
enableMouseInteraction?: boolean;
className?: string;
}
const vertexShader = `
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragmentShader = `
precision highp float;
uniform float iTime;
uniform vec3 iResolution;
uniform vec3 uColor;
uniform float uAmplitude;
uniform float uDistance;
uniform vec2 uMouse;
#define PI 3.1415926538
const int u_line_count = 40;
const float u_line_width = 7.0;
const float u_line_blur = 10.0;
float Perlin2D(vec2 P) {
vec2 Pi = floor(P);
vec4 Pf_Pfmin1 = P.xyxy - vec4(Pi, Pi + 1.0);
vec4 Pt = vec4(Pi.xy, Pi.xy + 1.0);
Pt = Pt - floor(Pt * (1.0 / 71.0)) * 71.0;
Pt += vec2(26.0, 161.0).xyxy;
Pt *= Pt;
Pt = Pt.xzxz * Pt.yyww;
vec4 hash_x = fract(Pt * (1.0 / 951.135664));
vec4 hash_y = fract(Pt * (1.0 / 642.949883));
vec4 grad_x = hash_x - 0.49999;
vec4 grad_y = hash_y - 0.49999;
vec4 grad_results = inversesqrt(grad_x * grad_x + grad_y * grad_y)
* (grad_x * Pf_Pfmin1.xzxz + grad_y * Pf_Pfmin1.yyww);
grad_results *= 1.4142135623730950;
vec2 blend = Pf_Pfmin1.xy * Pf_Pfmin1.xy * Pf_Pfmin1.xy
* (Pf_Pfmin1.xy * (Pf_Pfmin1.xy * 6.0 - 15.0) + 10.0);
vec4 blend2 = vec4(blend, vec2(1.0 - blend));
return dot(grad_results, blend2.zxzx * blend2.wwyy);
}
float pixel(float count, vec2 resolution) {
return (1.0 / max(resolution.x, resolution.y)) * count;
}
float lineFn(vec2 st, float width, float perc, float offset, vec2 mouse, float time, float amplitude, float distance) {
float split_offset = (perc * 0.4);
float split_point = 0.1 + split_offset;
float amplitude_normal = smoothstep(split_point, 0.7, st.x);
float amplitude_strength = 0.5;
float finalAmplitude = amplitude_normal * amplitude_strength
* amplitude * (1.0 + (mouse.y - 0.5) * 0.2);
float time_scaled = time / 10.0 + (mouse.x - 0.5) * 1.0;
float blur = smoothstep(split_point, split_point + 0.05, st.x) * perc;
float xnoise = mix(
Perlin2D(vec2(time_scaled, st.x + perc) * 2.5),
Perlin2D(vec2(time_scaled, st.x + time_scaled) * 3.5) / 1.5,
st.x * 0.3
);
float y = 0.5 + (perc - 0.5) * distance + xnoise / 2.0 * finalAmplitude;
float line_start = smoothstep(
y + (width / 2.0) + (u_line_blur * pixel(1.0, iResolution.xy) * blur),
y,
st.y
);
float line_end = smoothstep(
y,
y - (width / 2.0) - (u_line_blur * pixel(1.0, iResolution.xy) * blur),
st.y
);
return clamp(
(line_start - line_end) * (1.0 - smoothstep(0.0, 1.0, pow(perc, 0.3))),
0.0,
1.0
);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float line_strength = 1.0;
for (int i = 0; i < u_line_count; i++) {
float p = float(i) / float(u_line_count);
line_strength *= (1.0 - lineFn(
uv,
u_line_width * pixel(1.0, iResolution.xy) * (1.0 - p),
p,
(PI * 1.0) * p,
uMouse,
iTime,
uAmplitude,
uDistance
));
}
float colorVal = 1.0 - line_strength;
fragColor = vec4(uColor * colorVal, colorVal);
}
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
`;
const Threads: React.FC<ThreadsProps> = ({
color = [1, 1, 1],
amplitude = 1,
distance = 0,
enableMouseInteraction = false,
className = '',
...rest
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const animationFrameId = useRef<number | null>(null);
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
const renderer = new Renderer({ alpha: true });
const gl = renderer.gl;
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
container.appendChild(gl.canvas);
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex: vertexShader,
fragment: fragmentShader,
uniforms: {
iTime: { value: 0 },
iResolution: {
value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)
},
uColor: { value: new Color(...color) },
uAmplitude: { value: amplitude },
uDistance: { value: distance },
uMouse: { value: new Float32Array([0.5, 0.5]) }
}
});
const mesh = new Mesh(gl, { geometry, program });
function resize() {
const { clientWidth, clientHeight } = container;
renderer.setSize(clientWidth, clientHeight);
program.uniforms.iResolution.value.r = clientWidth;
program.uniforms.iResolution.value.g = clientHeight;
program.uniforms.iResolution.value.b = clientWidth / clientHeight;
}
window.addEventListener('resize', resize);
resize();
const currentMouse = new Float32Array([0.5, 0.5]);
const targetMouse = new Float32Array([0.5, 0.5]);
function handleMouseMove(e: MouseEvent) {
const rect = container.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = 1.0 - (e.clientY - rect.top) / rect.height;
targetMouse[0] = x;
targetMouse[1] = y;
}
function handleMouseLeave() {
targetMouse[0] = 0.5;
targetMouse[1] = 0.5;
}
if (enableMouseInteraction) {
container.addEventListener('mousemove', handleMouseMove);
container.addEventListener('mouseleave', handleMouseLeave);
}
function update(t: number) {
if (enableMouseInteraction) {
const smoothing = 0.05;
currentMouse[0] += smoothing * (targetMouse[0] - currentMouse[0]);
currentMouse[1] += smoothing * (targetMouse[1] - currentMouse[1]);
program.uniforms.uMouse.value[0] = currentMouse[0];
program.uniforms.uMouse.value[1] = currentMouse[1];
} else {
program.uniforms.uMouse.value[0] = 0.5;
program.uniforms.uMouse.value[1] = 0.5;
}
program.uniforms.iTime.value = t * 0.001;
renderer.render({ scene: mesh });
animationFrameId.current = requestAnimationFrame(update);
}
animationFrameId.current = requestAnimationFrame(update);
return () => {
if (animationFrameId.current !== null) cancelAnimationFrame(animationFrameId.current);
window.removeEventListener('resize', resize);
if (enableMouseInteraction) {
container.removeEventListener('mousemove', handleMouseMove);
container.removeEventListener('mouseleave', handleMouseLeave);
}
if (container.contains(gl.canvas)) container.removeChild(gl.canvas);
gl.getExtension('WEBGL_lose_context')?.loseContext();
};
}, [color, amplitude, distance, enableMouseInteraction]);
return <div ref={containerRef} className={`w-full h-full relative ${className}`} {...rest} />;
};
export default Threads;

View File

@@ -4,8 +4,9 @@ import Link from 'next/link';
const FOOTER_MENU_ITEMS = [
{ name: 'About', path: '#about' },
{ name: 'Experience', path: '#experience' },
{ name: 'Skills', path: '#skills' },
{ name: 'Projects', path: '#projects' },
{ name: 'Monitoring', path: '#monitoring' },
{ name: 'Contact', path: '#contact' },
];

View File

@@ -8,71 +8,54 @@ import { LanguageToggle } from '@/components/ui/language-toggle';
import { useTranslations } from 'next-intl';
const HEADER_MENU_ITEMS = [
{ key: 'about', path: '/#about', isScroll: true },
{ key: 'skills', path: '/#skills', isScroll: true },
{ key: 'projects', path: '/#projects', isScroll: true },
{ key: 'monitoring', path: '/#monitoring', isScroll: true },
{ key: 'contact', path: '/#contact', isScroll: true },
{ key: 'about', path: '/#about' },
{ key: 'skills', path: '/#skills' },
{ key: 'projects', path: '/#projects' },
{ key: 'monitoring', path: '/#monitoring' },
{ key: 'contact', path: '/#contact' },
];
interface HeaderProfileProps {
showImage?: boolean;
showName?: boolean;
imageClassName?: string;
hasOrder?: boolean;
}
const HeaderProfile = ({
showImage = true,
showName = true,
imageClassName,
hasOrder = true,
}: HeaderProfileProps) => {
const HeaderProfile = () => {
return (
<Link
href="/"
className={hasOrder ? 'order-2 tablet:order-1' : ''}
>
<div className="flex items-center gap-3 smalltablet:gap-4">
{showImage && (
<div
className={`w-9 h-9 smalltablet:w-10 smalltablet:h-10 desktop:w-14 desktop:h-14 rounded-full bg-gray-300 dark:bg-gray-600 ${imageClassName || ''}`}
/>
)}
{showName && (
<h1 className="text-sm smalltablet:text-base desktop:text-lg transition-colors font-bold">
MINJO KIM
</h1>
)}
</div>
<Link href="/#top" onClick={(e) => handleScrollClick(e, '/#top')} >
<h1 className="text-base smalltablet:text-lg desktop:text-lg font-bold">
MINJO
</h1>
</Link>
);
};
const handleScrollClick = (
e: React.MouseEvent<HTMLAnchorElement>,
path: string,
) => {
e.preventDefault();
const sectionId = path.replace('/#', '');
const element = document.getElementById(sectionId);
const navbarHeight = 70;
if (element) {
const elementPosition = element.getBoundingClientRect().top + window.scrollY;
const offsetPosition = elementPosition - navbarHeight;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
} else {
// 요소를 찾지 못하면 최상단으로 스크롤
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
};
const HeaderMenuItemsDesktop = () => {
const t = useTranslations('header');
const handleScrollClick = (e: React.MouseEvent<HTMLAnchorElement>, path: string, isScroll: boolean) => {
if (isScroll) {
e.preventDefault();
const sectionId = path.replace('/#', '');
const element = document.getElementById(sectionId);
if (element) {
const navbarHeight = 70;
const elementPosition = element.getBoundingClientRect().top + window.scrollY;
const offsetPosition = elementPosition - navbarHeight;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}
};
return (
<div className="flex justify-center items-center">
<div className="tablet:flex justify-center items-center hidden">
{HEADER_MENU_ITEMS.map((item) => (
<div
key={item.key}
@@ -80,7 +63,7 @@ const HeaderMenuItemsDesktop = () => {
>
<Link
href={item.path}
onClick={(e) => handleScrollClick(e, item.path, item.isScroll)}
onClick={(e) => handleScrollClick(e, item.path)}
className="font-extralight text-xs tablet:text-sm desktop:text-base duration-100 ease-in hover:border-b-2 hover:border-b-black hover:dark:border-b-white pb-1"
>
{t(item.key)}
@@ -95,71 +78,36 @@ const HeaderMenuItemsMobile = () => {
const [isOpen, setIsOpen] = useState(false);
const t = useTranslations('header');
const handleScrollClick = (e: React.MouseEvent<HTMLAnchorElement>, path: string, isScroll: boolean) => {
if (isScroll) {
e.preventDefault();
const sectionId = path.replace('/#', '');
const element = document.getElementById(sectionId);
if (element) {
const navbarHeight = 70;
const elementPosition = element.getBoundingClientRect().top + window.scrollY;
const offsetPosition = elementPosition - navbarHeight;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}
const handleMenuClick = (e: React.MouseEvent<HTMLAnchorElement>, path: string) => {
handleScrollClick(e, path);
setIsOpen(false);
};
return (
<>
<button
onClick={() => setIsOpen(true)}
className="dark:invert hover:cursor-pointer"
aria-label="Open menu"
onClick={() => setIsOpen(!isOpen)}
className="cursor-pointer"
>
<Menu className="w-6 h-6" />
{isOpen ? (
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
)}
</button>
{isOpen && (
<div className="fixed inset-0 z-50 bg-background/95 backdrop-blur">
<div className="flex flex-col h-full">
<div className="flex justify-end p-4">
<button
onClick={() => setIsOpen(false)}
className="dark:invert hover:cursor-pointer"
aria-label="Close menu"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="flex flex-col items-center justify-between h-full py-12 smalltablet:py-16">
<div className="flex flex-col w-full h-full justify-center items-center gap-16 smalltablet:gap-20">
<HeaderProfile
showImage={false}
hasOrder={false}
/>
<div className="flex flex-col items-center gap-8 smalltablet:gap-12">
{HEADER_MENU_ITEMS.map(item => (
<div
key={item.key}
className="px-4 smalltablet:px-6 desktop:px-8 transition-all items-center"
>
<Link
href={item.path}
onClick={(e) => handleScrollClick(e, item.path, item.isScroll)}
className="text-lg smalltablet:text-xl duration-100 ease-in hover:border-b-2 hover:border-b-brand-primary hover:dark:border-b-white pb-1"
>
{t(item.key)}
</Link>
</div>
))}
</div>
</div>
</div>
<div className="fixed top-[70px] left-0 right-0 z-50 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 border-b animate-in fade-in fade-out transition-all duration-300">
<div className="flex flex-col items-center gap-6 py-6">
{HEADER_MENU_ITEMS.map((item) => (
<Link
href={item.path}
key={item.key}
onClick={(e) => handleMenuClick(e, item.path)}
className="text-lg smalltablet:text-xl duration-100 ease-in hover:border-b-2 hover:border-b-brand-primary hover:dark:border-b-white pb-1"
>
{t(item.key)}
</Link>
))}
</div>
</div>
)}
@@ -170,19 +118,29 @@ const HeaderMenuItemsMobile = () => {
const Header = () => {
return (
<header
className="fixed h-[70px] top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60"
className="fixed h-[70px] top-0 z-60 w-full border-b border-border bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60"
>
<div className="flex justify-between items-center tracking-wider tablet:tracking-widest font-brand-book px-4 smalltablet:px-6 desktop:px-8 py-4 max-w-[1920px] mx-auto">
<HeaderProfile
showImage={false}
/>
<div className="order-1 tablet:order-2 flex items-center">
<div className="tablet:hidden">
<HeaderMenuItemsMobile />
</div>
<div className="hidden tablet:block">
<HeaderMenuItemsDesktop />
</div>
<div className="flex justify-between items-center tracking-wider tablet:tracking-widest font-brand-book px-4 smalltablet:px-6 desktop:px-8 py-4 max-w-[1920px] mx-auto relative">
{/* 모바일: 왼쪽 - 햄버거 아이콘 */}
<div className="tablet:hidden flex items-center relative">
<HeaderMenuItemsMobile />
</div>
{/* 모바일: 가운데 - 이름, 태블릿 이상: 왼쪽 - 이름 */}
<div className="tablet:hidden absolute left-1/2 -translate-x-1/2">
<HeaderProfile />
</div>
<div className="hidden tablet:block">
<HeaderProfile />
</div>
{/* 모바일: 오른쪽 - 토글 버튼들, 태블릿 이상: 오른쪽 - 메뉴 + 토글 버튼들 */}
<div className="flex items-center relative tablet:hidden">
<LanguageToggle />
<ModeToggle />
</div>
<div className="hidden tablet:flex items-center">
<HeaderMenuItemsDesktop />
<LanguageToggle />
<ModeToggle />
</div>