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
47 changes: 47 additions & 0 deletions src/components/home/CharacterSearchSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { FiSearch } from 'react-icons/fi'
import { useCharacterSearch } from '../../hooks/search/useCharacterSearch'
import Button from '../common/Button'

const CharacterSearchSection = () => {
const {
searchCharacterHandler,
characterName,
setCharacterName,
searchLoading
} = useCharacterSearch()
return (
<div className="bg-white rounded-xl p-5 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden h-fit self-start">
<div className="flex flex-col justify-between">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 bg-gradient-to-br from-blue-100 to-blue-200 rounded-lg flex items-center justify-center shadow-sm">
<FiSearch className="text-blue-600 text-lg" />
</div>
<h2 className="text-base font-semibold text-gray-800">캐릭터 검색</h2>
</div>
<div className="space-y-3">
<input
type="text"
placeholder="캐릭터 이름을 입력하세요"
value={characterName}
onChange={e => setCharacterName(e.target.value)}
className="w-full px-4 py-2.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm bg-white"
/>
<Button
size="medium"
scheme="solid"
disabled={searchLoading}
className={`w-full text-sm bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 transition-all shadow-sm ${
searchLoading
? 'bg-blue-300 cursor-not-allowed'
: 'bg-blue-500 hover:bg-blue-600'
}`}
onClick={searchCharacterHandler}>
{searchLoading ? '검색 중...' : '검색'}
</Button>
</div>
</div>
</div>
)
}

export default CharacterSearchSection
64 changes: 64 additions & 0 deletions src/components/home/FeatureShowcase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { FiCalendar } from 'react-icons/fi'
import { useAuthStore } from '../../store/authStore'
import { useUserStore } from '../../store/userStore'
import { useNavigate } from 'react-router-dom'
import { guest } from '../../data/guest'
import Button from '../common/Button'

const FeatureShowcase = () => {
const { storeLogin } = useAuthStore()
const { setCharacterOcid } = useUserStore()
const nav = useNavigate()

const handleGuestLogin = async () => {
await storeLogin('', '', 'guest')
setCharacterOcid(guest.ocid)
nav('/character')
}

return (
<div className="bg-white rounded-xl p-5 shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
<FiCalendar className="text-green-600 text-lg" />
</div>
<h2 className="text-base font-semibold">체험하기</h2>
</div>
<div className="space-y-3">
<div className="space-y-1.5 text-sm">
<p className="font-medium text-gray-900">비로그인 이용 가능한 기능</p>
<div className="flex items-center gap-2 text-gray-600">
<span className="w-1 h-1 rounded-full bg-blue-500" />
<span>캐릭터 정보 조회</span>
</div>
<div className="flex items-center gap-2 text-gray-600">
<span className="w-1 h-1 rounded-full bg-blue-500" />
<span>길드 정보 조회</span>
</div>
</div>
<div className="space-y-1.5 text-sm">
<p className="font-medium text-gray-900">
로그인 후 이용 가능한 기능
</p>
<div className="flex items-center gap-2 text-gray-600">
<span className="w-1 h-1 rounded-full bg-gray-400" />
<span>캘린더로 일정 관리하기</span>
</div>
<div className="flex items-center gap-2 text-gray-600">
<span className="w-1 h-1 rounded-full bg-gray-400" />
<span>길드원 관리하기</span>
</div>
</div>
<Button
size="medium"
scheme="solid"
className="w-full text-sm"
onClick={handleGuestLogin}>
게스트로 시작하기
</Button>
</div>
</div>
)
}

export default FeatureShowcase
119 changes: 119 additions & 0 deletions src/components/home/GuildSearchSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useNavigate } from 'react-router-dom'
import { FiUsers } from 'react-icons/fi'
import { useGuildSearch } from '../../hooks/search/useGuildSearch'
import { servers } from '../../data/worlds'
import Button from '../common/Button'

const GuildSearchSection = () => {
const nav = useNavigate()
const {
selectedServer,
setSelectedServer,
guildList,
searchGuildHandler,
addGuildList,
removeGuildList,
handleGuildKeyPress,
guildName,
setGuildName
} = useGuildSearch()

const onSearchGuild = async () => {
nav('/searchGuild')
searchGuildHandler()
}

return (
<div className="col-span-1 lg:col-span-2 lg:max-w-4xl lg:mx-auto bg-white rounded-xl p-4 sm:p-5 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
<div className="flex flex-col justify-between">
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-8 bg-gradient-to-br from-purple-100 to-purple-200 rounded-lg flex items-center justify-center shadow-sm">
<FiUsers className="text-purple-600 text-lg" />
</div>
<h2 className="text-sm sm:text-base font-semibold text-gray-800">
길드 검색 <span className="text-xs text-gray-500">(최대 4개)</span>
</h2>
</div>

<div className="space-y-4">
{/* 서버 선택과 입력 필드 */}
<div className="flex flex-col sm:flex-row gap-3">
<select
value={selectedServer}
onChange={e => setSelectedServer(e.target.value)}
className="w-full sm:w-auto px-3 py-3 sm:py-2.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 bg-white min-w-[140px]">
<option value="">서버 선택</option>
{servers.map(server => (
<option
key={server.id}
value={server.id}>
{server.name}
</option>
))}
</select>

<div className="flex gap-2 flex-1">
<input
type="text"
placeholder="길드 이름을 입력하세요"
value={guildName}
onChange={e => setGuildName(e.target.value)}
onKeyPress={handleGuildKeyPress}
className="flex-1 px-4 py-3 sm:py-2.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 bg-white"
/>
<Button
size="medium"
scheme="solid"
onClick={() => addGuildList(guildName)}
className="px-4 py-3 sm:py-2.5 bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-sm transition-all shadow-sm whitespace-nowrap">
추가
</Button>
</div>
</div>

{/* 길드 목록 */}
{guildList.length > 0 && (
<div className="space-y-2">
<p className="text-xs text-gray-600">검색할 길드 목록</p>
<div className="flex flex-wrap gap-2">
{guildList.map(guild => (
<div
key={guild}
className="flex items-center gap-1 px-3 py-2 bg-gradient-to-r from-purple-50 to-purple-100 border border-purple-200 rounded-lg group">
<span className="text-sm text-purple-700">{guild}</span>
<button
onClick={() => removeGuildList(guild)}
className="p-1 text-purple-400 hover:text-purple-600 rounded-full hover:bg-purple-100 transition-colors">
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
))}
</div>
</div>
)}

<Button
size="medium"
scheme="solid"
onClick={onSearchGuild}
className="w-full py-3 sm:py-2.5 text-sm bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 transition-all shadow-sm font-medium">
길드 검색하기
</Button>
</div>
</div>
</div>
)
}

export default GuildSearchSection
84 changes: 84 additions & 0 deletions src/components/home/HeroSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../../hooks/useAuth'
import { useAuthStore } from '../../store/authStore'
import { useUserStore } from '../../store/userStore'
import { guest } from '../../data/guest'
import Logo from '../../assets/logo.png'
import GoogleLogo from '../../assets/gogle.svg'
import { useShallow } from 'zustand/react/shallow'

const HeroSection = () => {
const { userLogin, isLoading } = useAuth()
const storeLogin = useAuthStore(s => s.storeLogin)
const [setUserInfo, setCharacterOcid] = useUserStore(
useShallow(s => [s.setUserInfo, s.setCharacterOcid])
)
const nav = useNavigate()

const handleGuestLogin = async () => {
try {
await storeLogin('', '', 'guest')
setCharacterOcid(guest.ocid)
nav('/character')
} catch (error) {
console.error('게스트 로그인 실패:', error)
alert('게스트 로그인에 실패했습니다.')
}
}

const handleMemberLogin = async () => {
const userInfo = await userLogin()
if (userInfo) {
setUserInfo(userInfo)
setCharacterOcid(userInfo.ocid!)
const redirectPath = userInfo.nexonApiKey ? '/character' : '/signup'
nav(redirectPath)
}
}

return (
<div className="text-center mb-8 sm:mb-12">
<div className="flex flex-col items-center mb-6">
<img
src={Logo}
alt="메이플링크 로고"
className="w-12 h-12 sm:w-16 sm:h-16 mb-4"
/>
<div className="inline-flex items-center gap-2 px-3 sm:px-4 py-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-full border border-blue-100">
<span className="w-1.5 h-1.5 rounded-full bg-blue-400" />
<span className="text-xs sm:text-sm text-blue-600 font-medium">
메이플스토리 통합 관리 플랫폼
</span>
</div>
</div>
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 mb-3 px-4">
메이플스토리를
<br className="sm:hidden" /> 더 스마트하게
</h1>
<p className="text-base sm:text-lg text-gray-500 px-4">
캐릭터부터 길드까지, 한눈에 관리하세요
</p>
<div className="flex flex-col sm:flex-row justify-center gap-3 sm:gap-4 mt-6 sm:mt-8 px-4">
<button
onClick={handleGuestLogin}
disabled={isLoading}
className="w-full sm:w-auto px-5 py-3 sm:py-2.5 bg-white text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed">
체험하기
</button>
<button
onClick={handleMemberLogin}
disabled={isLoading}
className="w-full sm:w-auto px-5 py-3 sm:py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center justify-center gap-2 font-medium disabled:opacity-50 disabled:cursor-not-allowed">
<img
src={GoogleLogo}
alt="Google"
className="w-5 h-5"
/>
{isLoading ? '로그인 중...' : 'Google로 시작하기'}
</button>
</div>
</div>
)
}

export default HeroSection
78 changes: 78 additions & 0 deletions src/components/home/HomeNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { FiMenu, FiX } from 'react-icons/fi'
import Logo from '../../assets/logo.png'
import KakaoOpenChatButton from '../common/KakaoOpenChatButton'

const KAKAO_CHAT_LINK = 'https://open.kakao.com/o/s4tfG2Ah'

const HomeNavigation = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false)

return (
<nav className="bg-white/80 backdrop-blur-sm border-b border-gray-200 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center gap-2">
<img
src={Logo}
className="w-8 h-8"
alt="MapleLink Logo"
/>
<span className="font-bold text-lg sm:text-xl text-gray-900">
MapleLink
</span>
</div>

<div className="hidden sm:flex items-center gap-4">
<KakaoOpenChatButton
chatLink={KAKAO_CHAT_LINK}
className="!py-1.5 !px-3 text-sm"
/>
<Link
to="/apiGuide"
className="text-gray-600 hover:text-blue-600 font-medium text-sm whitespace-nowrap">
API 가이드
</Link>
<Link
to="/faq"
className="text-gray-600 hover:text-blue-600 font-medium text-sm">
FAQ
</Link>
</div>

<button
className="sm:hidden p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100"
onClick={() => setIsMenuOpen(!isMenuOpen)}>
{isMenuOpen ? <FiX size={20} /> : <FiMenu size={20} />}
</button>
</div>

{isMenuOpen && (
<div className="sm:hidden border-t border-gray-200 py-4 bg-white/95 backdrop-blur-sm">
<div className="flex flex-col space-y-3">
<KakaoOpenChatButton
chatLink={KAKAO_CHAT_LINK}
className="!py-2 !px-4 text-sm mx-auto"
/>
<Link
to="/apiGuide"
className="text-gray-600 hover:text-blue-600 font-medium text-sm text-center py-2"
onClick={() => setIsMenuOpen(false)}>
API 가이드
</Link>
<Link
to="/faq"
className="text-gray-600 hover:text-blue-600 font-medium text-sm text-center py-2"
onClick={() => setIsMenuOpen(false)}>
FAQ
</Link>
</div>
</div>
)}
</div>
</nav>
)
}

export default HomeNavigation
Loading