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
26 changes: 21 additions & 5 deletions src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@import "tailwindcss";

@import url('https://cdn.jsdelivr.net/gh/moonspam/NanumSquare@2.0/nanumsquare.css');

:root {
--background: #ffffff;
Expand All @@ -25,10 +25,6 @@ body {
color: var(--foreground);
}

body {
font-family: var(--font-nanum-gothic), sans-serif;
}

/* Markdown Editor Toolbar Size Customization */
.w-md-editor-toolbar {
padding: 8px 10px !important;
Expand All @@ -44,3 +40,23 @@ body {
width: 18px !important;
height: 18px !important;
}

/* Bubble animation */
.bubble {
position: absolute;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
pointer-events: none;
animation: bubble-rise 2s ease-out forwards;
}

@keyframes bubble-rise {
from {
transform: translateY(0) scale(1);
opacity: 1;
}
to {
transform: translateY(-200px) scale(0.5);
opacity: 0;
}
}
10 changes: 1 addition & 9 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,11 @@ import type { Metadata } from "next";
import "./globals.css";
import ClientAuthInit from '@/components/ClientAuthInit';
import NavBar from '@/components/Layout/NavBar'
import { Nanum_Gothic } from 'next/font/google';
import { Analytics } from "@vercel/analytics/next"
import { SpeedInsights } from "@vercel/speed-insights/next"
import Script from 'next/script'
import { getSiteUrl, toAbsoluteUrl } from '@/utils/seo'

const nanumGothic = Nanum_Gothic({
subsets: ['latin'],
weight: ['400', '700', '800'], // 필요한 굵기만 선택
variable: '--font-nanum-gothic', // CSS 변수명으로 Tailwind와 연동 가능
display: 'swap',
});

export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
title: {
Expand Down Expand Up @@ -50,7 +42,7 @@ export default function RootLayout({
<html lang="ko">
<body
suppressHydrationWarning
className={`${nanumGothic.variable} font-nanum antialiased`}
className={`font-nanum antialiased`}
>
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-3B7CWRNZ9F"
Expand Down
152 changes: 104 additions & 48 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,36 @@ import { useAuthStore, selectLogin, selectIsLogin } from '@/store/AuthStore'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/Common/Button'

import BubbleBackground from '@/components/Home/BubbleBackground'

// 아이콘 SVG 컴포넌트
const MailIcon = () => (
<svg className="w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
</svg>
);
const LockIcon = () => (
<svg className="w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</svg>
);

// Bubblog 로고 아이콘 (색상을 모노톤 그라데이션으로 변경)
const BubblogLogoIcon = () => (
<svg width="36" height="36" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.33331 16C5.33331 15.6464 5.40254 15.2963 5.53674 14.965C6.46008 12.6521 8.52084 10.9388 11.0208 10.7139C11.3831 10.678 11.7228 10.4571 11.901 10.1341C12.9156 8.35623 14.7958 7.33331 16.9469 7.33331C19.6243 7.33331 21.841 8.85042 22.7687 11.0375C22.9138 11.3838 23.2503 11.6167 23.6333 11.6167H23.6666C25.9316 11.6167 27.7666 13.4517 27.7666 15.7167C27.7666 17.5833 26.5416 19.1583 24.8916 19.6416C24.5166 19.7583 24.2333 20.1083 24.2333 20.5083V20.5083C24.2333 22.4416 22.675 24 20.7416 24H11.3333C7.98331 24 5.33331 21.35 5.33331 18V16Z" fill="url(#grayGradient)" />
<defs>
<linearGradient id="grayGradient" x1="16.55" y1="7.33331" x2="16.55" y2="24" gradientUnits="userSpaceOnUse">
<stop stopColor="#4B5563" /> {/* Gray 600 */}
<stop offset="1" stopColor="#1F2937" /> {/* Gray 800 */}
</linearGradient>
</defs>
</svg>
);

export default function LoginPage() {
const login = useAuthStore(selectLogin);
const isAuthenticated = useAuthStore(selectIsLogin);
const login = useAuthStore(selectLogin)
const isAuthenticated = useAuthStore(selectIsLogin)
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
Expand All @@ -26,59 +53,88 @@ export default function LoginPage() {
await login({ email, password })
router.push('/')
} catch (err: any) {
setError(err.message)
setError('이메일 또는 비밀번호를 확인해주세요.')
}
}

return (
<form
onSubmit={onSubmit}
className="w-full max-w-md mt-8 bg-white rounded-2xl shadow-xl p-8 sm:p-10"
>
<h2 className="text-3xl font-bold text-gray-800 text-center mb-8">
로그인
</h2>
<div className="relative w-full flex flex-col items-center justify-center min-h-screen px-4 py-8 bg-gradient-to-br from-blue-500 to-purple-600 overflow-hidden">
<BubbleBackground />
<div className="relative z-10 w-full max-w-lg">

<div className="bg-white rounded-2xl shadow-2xl p-8 sm:p-10">

<div className="text-center">
<div className="flex items-center justify-center gap-3 mb-2">
<BubblogLogoIcon />
<h1 className="text-3xl font-bold text-gray-800">Welcome to Bubblog</h1>
</div>
<p className="text-lg text-gray-500 mt-2">
로그인하고 대화를 시작해 보세요
</p>
<h2 className="text-2xl font-bold text-gray-800 mt-8 mb-6">로그인</h2>
</div>

{error && (
<p className="text-sm text-red-500 text-center mb-4">
{error}
</p>
)}
<form onSubmit={onSubmit} className="space-y-6">
{error && (
<p className="text-base text-red-600 bg-red-100 p-3 rounded-lg text-center">
{error}
</p>
)}

<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
이메일
</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
placeholder="you@example.com"
className="w-full px-4 py-3 rounded-xl border border-gray-300 focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition"
/>
</div>
<div>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none">
<MailIcon />
</div>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="이메일 주소"
className="w-full pl-12 pr-4 py-3 rounded-lg border border-gray-200 bg-gray-50 focus:bg-white focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition text-lg"
/>
</div>
</div>

<div className="mb-8">
<label className="block text-sm font-medium text-gray-700 mb-2">
비밀번호
</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
placeholder="••••••••"
className="w-full px-4 py-3 rounded-xl border border-gray-300 focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition"
/>
</div>
<div>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none">
<LockIcon />
</div>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
placeholder="비밀번호"
className="w-full pl-12 pr-4 py-3 rounded-lg border border-gray-200 bg-gray-50 focus:bg-white focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition text-lg"
/>
</div>
</div>

<Button
type="submit"
className="w-full py-3 rounded-xl bg-blue-600 text-white font-semibold hover:bg-blue-700 active:bg-blue-800 transition"
>
로그인
</Button>
</form>
{/* 버튼 색상을 모노톤 그라데이션으로 변경 */}
<Button
type="submit"
className="w-full py-3 rounded-lg bg-gradient-to-r from-gray-700 to-gray-900 text-white font-bold shadow-lg hover:shadow-xl hover:-translate-y-0.5 transition-all duration-300 text-xl"
>
로그인
</Button>
</form>
</div>

<div className="text-center mt-8">
<p className="text-base text-gray-200">
아직 계정이 없으신가요?{' '}
<a href="/signup" className="font-semibold text-white hover:text-gray-200 hover:underline transition-colors text-base">
회원가입
</a>
</p>
</div>
</div>
</div>
)
}
4 changes: 2 additions & 2 deletions src/app/post/[postId]/PostDetailClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getBlogById, BlogDetail, putPostView, putPostLike } from '@/apis/blogAp

import { PostDetailHeader } from '@/components/PostDetail/Header';
import { PostDetailActions } from '@/components/PostDetail/Action';
import { PostDetailBody } from '@/components/PostDetail/Body';
import { PostNavbar } from '@/components/PostDetail/PostNavbar';
import { DraggableModal } from '@/components/Common/DraggableModal';
import { ChatViewButton } from '@/components/Chat/ChatViewButton';
Expand Down Expand Up @@ -63,8 +64,7 @@ export default function PostDetailClient({ postId }: { postId: string }) {
const isMyPost = post.userId === authUserId;

return (
<>
{isMyPost && <PostDetailActions postId={post.id} />}
<div className="w-full max-w-4xl mx-auto px-4">
<PostDetailHeader post={post}>
<ChatViewButton
userId={post.userId}
Expand Down
91 changes: 49 additions & 42 deletions src/components/Home/BubbleBackground.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,56 @@
// 출처 : https://reactbits.dev/animations/animated-content
'use client'

import React, { useEffect, useRef } from 'react'
import { gsap } from 'gsap'
import { useCallback, useEffect, useRef } from 'react'

export default function BubbleBackground() {
const container = useRef<HTMLDivElement>(null)
const BubbleBackground = () => {
const containerRef = useRef<HTMLDivElement>(null)

useEffect(() => {
const bubbles = container.current?.querySelectorAll<HTMLElement>('.bubble')
if (!bubbles) return

bubbles.forEach(bubble => {
const size = gsap.utils.random(20, 80) // 크기 랜덤
const opacity = gsap.utils.random(0.1, 0.3) // 불투명도 랜덤
const leftPct = gsap.utils.random(0, 100) // 가로 위치 랜덤(%)

// 초기 위치 세팅: 화면 아래(음수 값), left 으로 수평 분산
gsap.set(bubble, {
width: size,
height: size,
opacity,
bottom: -size,
left: `${leftPct}%`,
backgroundColor: '#9CA3AF'
})

gsap.to(bubble, {
bottom: '100%',
duration: gsap.utils.random(6, 12),
ease: 'none',
repeat: -1,
delay: gsap.utils.random(0, 5)
})
})
const createBubble = useCallback((x: number, y: number) => {
const container = containerRef.current
if (!container) return

const bubble = document.createElement('span')
const size = Math.random() * 30 + 10 // 10px to 40px

bubble.className = 'bubble'
bubble.style.width = `${size}px`
bubble.style.height = `${size}px`

// Adjust position to center the bubble on the cursor
bubble.style.left = `${x - container.getBoundingClientRect().left - size / 2}px`
bubble.style.top = `${y - container.getBoundingClientRect().top - size / 2}px`

container.appendChild(bubble)

setTimeout(() => {
bubble.remove()
}, 2000) // Animation duration is 2s
}, [])

return (
<div
ref={container}
className="fixed inset-0 pointer-events-none overflow-hidden"
>
{Array.from({ length: 30 }).map((_, i) => (
<div key={i} className="bubble rounded-full absolute" />
))}
</div>
const handleMouseMove = useCallback(
(e: MouseEvent) => {
// Throttle bubble creation
if (Math.random() > 0.5) {
createBubble(e.clientX, e.clientY)
}
},
[createBubble],
)
}

useEffect(() => {
const container = containerRef.current
if (container) {
container.addEventListener('mousemove', handleMouseMove)
}

return () => {
if (container) {
container.removeEventListener('mousemove', handleMouseMove)
}
}
}, [handleMouseMove])

return <div ref={containerRef} className="absolute inset-0 w-full h-full overflow-hidden" />
}

export default BubbleBackground
2 changes: 1 addition & 1 deletion src/components/PostDetail/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import rehypeHighlight from 'rehype-highlight';

export function PostDetailBody({ content }: { content: string }) {
return (
<div data-color-mode="light" className="prose prose-sm sm:prose-base max-w-none p-6">
<div data-color-mode="light" className="prose prose-sm sm:prose-base max-w-none p-6 font-sans">
<Markdown
source={content}
rehypePlugins={[[rehypeHighlight, { detect: true }]]}
Expand Down
2 changes: 1 addition & 1 deletion src/components/PostDetail/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function PostDetailHeader({ post, children }: Props) {
return (
<header className="mb-10 space-y-4">
{/* 제목 */}
<h1 className="text-4xl font-extrabold text-gray-900">
<h1 className="text-4xl font-extrabold text-gray-900 font-sans">
{post.title}
</h1>

Expand Down
2 changes: 1 addition & 1 deletion tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const config: Config = {
theme: {
extend: {
fontFamily: {
nanum: ['var(--font-nanum-gothic)'],
nanum: ['NanumSquareRound', 'sans-serif'],
},
},
},
Expand Down