diff --git a/src/components/character/CharacterPage.tsx b/src/components/character/CharacterPage.tsx deleted file mode 100644 index 66e8d89..0000000 --- a/src/components/character/CharacterPage.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import { useState } from 'react' -import { AbilitryContainer } from './AbilitryContainer' -import { CharacterInfoContainer } from './CharacterInfoContainer' -import { HyperStatContainer } from './HyperStatContainer' -import { InventoryContainer } from './inventory/InventoryContainer' -import { StatContainer } from './StatContainer' -import { useCharacterData } from '../../hooks/character/useCharacterData' -import { useInventory } from '../../hooks/character/useInventory' - -import { useUserStore } from '../../store/userStore' -import { searchCharacterOcid } from '../../apis/character/characterController' - -import { FiAlertTriangle, FiSearch } from 'react-icons/fi' -import Button from '../common/Button' - -interface CharacterPageProps { - type: 'character' | 'search' -} - -export const CharacterPage = ({ type }: CharacterPageProps) => { - const { characterStats, ability, hyperStat, basic, isLoading, error } = - useCharacterData() - const [characterName, setCharacterName] = useState('') - - const { inventory } = useInventory() - - const [showStats, setShowStats] = useState(true) - - const { setCharacterOcid } = useUserStore() - const [searchLoading, setSearchLoading] = useState(false) - - const searchCharacterHandler = async () => { - if (characterName.trim() === '') { - alert('캐릭터 이름을 입력해주세요.') - return - } - setSearchLoading(true) - - try { - const { ocid } = await searchCharacterOcid(characterName.trim()) - if (!ocid) { - alert('캐릭터를 찾을 수 없습니다.') - return - } - setCharacterOcid(ocid) - } catch { - alert('캐릭터 검색에 실패했습니다.') - } finally { - setSearchLoading(false) - } - } - - if (error) { - return ( -
-
-
-
- {type === 'character' && ( -
-
- {/* */} -
-
-
-
-

- 본캐릭터는 넥슨 OpenAPI에서 레벨이 가장 높은 - 캐릭터를 기준으로 자동 설정됩니다. -

-

- - 정보가 정확하지 않다면 - - 동기화 - - 버튼을 눌러주세요 - -

-
-
-
-
-
- )} -
- -
-
-

- 캐릭터 정보를 불러올 수 없습니다 -

-

- 2023년 12월 21일 이후의 데이터만 조회할 수 있습니다. -

-
-
-
-
-
- -
-

- 캐릭터 검색 -

-
-
- 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" - /> - -
-
-
-
-
- ) - } - - return ( -
- {/* 탭 버튼 */} -
- {type === 'character' ? ( -
-
- {/* */} -
-
-
-
-

- 본캐릭터는 넥슨 OpenAPI에서 레벨이 가장 높은 캐릭터를 - 기준으로 자동 설정됩니다. -

-

- - 정보가 정확하지 않다면 - - 동기화 - 버튼을 눌러주세요 -

-
-
-
-
-
- ) : ( -
- )} -
- - -
- -
- setCharacterName?.(e.target.value)} - /> - -
-
- - {/* 콘텐츠 영역 */} - {isLoading ? ( -
- {showStats ? ( -
- {/* 캐릭터 정보와 어빌리티 스켈레톤 */} -
-
-
-
- - {/* 기본 스탯 스켈레톤 */} -
-
-
- - {/* 하이퍼 스탯 스켈레톤 */} -
-
-
-
- ) : ( -
- {/* 캐릭터 이미지 스켈레톤 */} -
- {/* 장비 정보 스켈레톤 */} -
- {Array.from({ length: 15 }).map((_, index) => ( -
- ))} -
-
- )} -
- ) : ( -
- {showStats ? ( -
- {/* 캐릭터 정보와 어빌리티 (큰 화면에서 세로로 배치) */} -
- {/* 캐릭터 정보 (작은 화면에서 맨 위) */} -
{basic && }
- - {/* 어빌리티 (캐릭터 정보 아래에 배치) */} -
{ability && }
-
- - {/* 기본 스탯 (큰 화면에서 중앙에 위치) */} -
- {characterStats && } -
- - {/* 하이퍼 스탯 (큰 화면에서 오른쪽에 위치) */} -
- {hyperStat && } -
-
- ) : ( -
- {inventory && basic?.character_image && ( - - )} -
- )} -
- )} -
- ) -} diff --git a/src/components/character/containers/CharacterDesktopHeader.tsx b/src/components/character/containers/CharacterDesktopHeader.tsx new file mode 100644 index 0000000..80031c4 --- /dev/null +++ b/src/components/character/containers/CharacterDesktopHeader.tsx @@ -0,0 +1,93 @@ +interface CharacterDesktopHeaderProps { + type: 'character' | 'search' + characterName: string + setCharacterName: (name: string) => void + searchLoading: boolean + onSearch: () => void + showStats: boolean + setShowStats: (show: boolean) => void +} + +export const CharacterDesktopHeader = ({ + type, + characterName, + setCharacterName, + searchLoading, + onSearch, + showStats, + setShowStats +}: CharacterDesktopHeaderProps) => { + return ( +
+ {type === 'character' ? ( +
+
+
+
+
+
+

+ 본캐릭터는 넥슨 OpenAPI에서 레벨이 가장 높은 캐릭터를 + 기준으로 자동 설정됩니다. +

+

+ + 정보가 정확하지 않다면 + + 동기화 + 버튼을 눌러주세요 +

+
+
+
+
+
+ ) : ( +
+ )} + + {/* 탭 버튼 */} +
+ + +
+ + {/* 검색 입력 */} +
+ setCharacterName(e.target.value)} + /> + +
+
+ ) +} diff --git a/src/components/character/CharacterInfoContainer.tsx b/src/components/character/containers/CharacterInfoContainer.tsx similarity index 93% rename from src/components/character/CharacterInfoContainer.tsx rename to src/components/character/containers/CharacterInfoContainer.tsx index 340e284..2fd6cfb 100644 --- a/src/components/character/CharacterInfoContainer.tsx +++ b/src/components/character/containers/CharacterInfoContainer.tsx @@ -1,5 +1,5 @@ -import { CharacterBasic } from '../../types/character' -import { formatDate } from '../../utils/format' +import { CharacterBasic } from '../../../types/character' +import { formatDate } from '../../../utils/format' interface Props { basic: CharacterBasic diff --git a/src/components/character/containers/CharacterMobileHeader.tsx b/src/components/character/containers/CharacterMobileHeader.tsx new file mode 100644 index 0000000..c527a5e --- /dev/null +++ b/src/components/character/containers/CharacterMobileHeader.tsx @@ -0,0 +1,70 @@ +interface CharacterMobileHeaderProps { + type: 'character' | 'search' + characterName: string + setCharacterName: (name: string) => void + searchLoading: boolean + onSearch: () => void + showStats: boolean + setShowStats: (show: boolean) => void +} + +export const CharacterMobileHeader = ({ + type, + characterName, + setCharacterName, + searchLoading, + onSearch, + showStats, + setShowStats +}: CharacterMobileHeaderProps) => { + return ( +
+
+ {/* 캐릭터 검색 */} +
+ setCharacterName(e.target.value)} + /> + +
+ + {/* 탭 네비게이션 */} +
+ + +
+ + {/* 도움말 (캐릭터 타입일 때만) */} + {type === 'character' && ( +
+

+ 💡 본캐는 최고레벨 캐릭터로 자동설정됩니다. 정확하지 않다면 동기화 + 버튼을 눌러주세요. +

+
+ )} +
+
+ ) +} diff --git a/src/components/character/inventory/ItemInventory.tsx b/src/components/character/inventory/ItemInventory.tsx index 9a279db..9071ffa 100644 --- a/src/components/character/inventory/ItemInventory.tsx +++ b/src/components/character/inventory/ItemInventory.tsx @@ -40,10 +40,10 @@ const ItemInventory = ({ inventory, characterImg, onSelected }: Props) => { const extraSlots = ['포켓 아이템', '뱃지'] return ( -
-
+
+
{/* 왼쪽 5행 2열 */} -
+
{leftSlots.map(slot => ( {
{/* 가운데 캐릭터 + 아래 슬롯 3개 */} -
-
+
+
-
+
{bottomSlots.map(slot => ( {
{/* 오른쪽 5행 2열 */} -
+
{rightSlots.map(slot => ( {
{/* 아래 포켓, 벳지 */} -
+
{extraSlots.map(slot => ( { return (
{ + return ( +
+ {showStats ? ( + <> + {/* 데스크톱 레이아웃 (lg 이상) */} +
+
+
{basic && }
+
{ability && }
+
+
+ {characterStats && } +
+
+ {hyperStat && } +
+
+ + {/* 모바일 레이아웃 (lg 미만) */} +
+ {basic && ( +
+ +
+ )} + {ability && ( +
+ +
+ )} + {characterStats && ( +
+ +
+ )} + {hyperStat && ( +
+ +
+ )} +
+ + ) : ( +
+ {inventory && basic?.character_image && ( + + )} +
+ )} +
+ ) +} diff --git a/src/components/character/pages/CharacterErrorState.tsx b/src/components/character/pages/CharacterErrorState.tsx new file mode 100644 index 0000000..d440f4e --- /dev/null +++ b/src/components/character/pages/CharacterErrorState.tsx @@ -0,0 +1,138 @@ +import { useState } from 'react' +import { FiAlertTriangle, FiSearch, FiInfo } from 'react-icons/fi' +import Button from '../../common/Button' +import { searchCharacterOcid } from '../../../apis/character/characterController' +import { useUserStore } from '../../../store/userStore' + +interface CharacterErrorStateProps { + type: 'character' | 'search' +} + +export const CharacterErrorState = ({ type }: CharacterErrorStateProps) => { + const [characterName, setCharacterName] = useState('') + const [searchLoading, setSearchLoading] = useState(false) + const { setCharacterOcid } = useUserStore() + + const searchCharacterHandler = async () => { + if (characterName.trim() === '') { + alert('캐릭터 이름을 입력해주세요.') + return + } + setSearchLoading(true) + + try { + const { ocid } = await searchCharacterOcid(characterName.trim()) + if (!ocid) { + alert('캐릭터를 찾을 수 없습니다.') + return + } + setCharacterOcid(ocid) + } catch { + alert('캐릭터 검색에 실패했습니다.') + } finally { + setSearchLoading(false) + } + } + + return ( +
+
+
+
+ {type === 'character' && ( +
+
+
+
+
+
+

+ 본캐릭터는 넥슨 OpenAPI에서 레벨이 가장 높은 캐릭터를 + 기준으로 자동 설정됩니다. +

+

+ + 정보가 정확하지 않다면 + + 동기화 + + 버튼을 눌러주세요 + +

+
+
+
+
+
+ )} +
+ +
+
+ +

+ 캐릭터 정보를 불러올 수 없습니다 +

+

+ 2023년 12월 21일 이후의 데이터만 조회할 수 있습니다. +

+ + {/* 모바일용 도움말 */} + {type === 'character' && ( +
+
+ +
+

+ 본캐릭터는 넥슨 OpenAPI에서 레벨이 가장 높은 캐릭터를 + 기준으로 자동 설정됩니다. +

+

+ 정보가 정확하지 않다면{' '} + 동기화 버튼을 + 눌러주세요. +

+
+
+
+ )} +
+ + {/* 검색 폼 */} +
+
+
+
+ +
+

+ 캐릭터 검색 +

+
+
+ 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" + /> + +
+
+
+
+
+ ) +} diff --git a/src/components/character/pages/CharacterLoadingSkeleton.tsx b/src/components/character/pages/CharacterLoadingSkeleton.tsx new file mode 100644 index 0000000..907b84c --- /dev/null +++ b/src/components/character/pages/CharacterLoadingSkeleton.tsx @@ -0,0 +1,63 @@ +interface CharacterLoadingSkeletonProps { + showStats: boolean +} + +export const CharacterLoadingSkeleton = ({ + showStats +}: CharacterLoadingSkeletonProps) => { + return ( +
+ {showStats ? ( + <> + {/* 데스크톱 스켈레톤 (lg 이상) */} +
+
+
+
+
+
+
+
+
+
+
+
+ + {/* 모바일 스켈레톤 (lg 미만) */} +
+
+
+
+
+
+ + ) : ( + <> + {/* 데스크톱 장비 스켈레톤 */} +
+
+
+ {Array.from({ length: 15 }).map((_, index) => ( +
+ ))} +
+
+ + {/* 모바일 장비 스켈레톤 */} +
+
+
+ {Array.from({ length: 15 }).map((_, index) => ( +
+ ))} +
+
+ + )} +
+ ) +} diff --git a/src/components/character/pages/CharacterPage.tsx b/src/components/character/pages/CharacterPage.tsx new file mode 100644 index 0000000..3129eee --- /dev/null +++ b/src/components/character/pages/CharacterPage.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react' +import { useCharacterData } from '../../../hooks/character/useCharacterData' +import { useInventory } from '../../../hooks/character/useInventory' +import { useUserStore } from '../../../store/userStore' +import { searchCharacterOcid } from '../../../apis/character/characterController' + +import { CharacterErrorState } from '../pages/CharacterErrorState' + +import { CharacterLoadingSkeleton } from '../pages/CharacterLoadingSkeleton' +import { CharacterContent } from '../pages/CharacterContent' +import { CharacterDesktopHeader } from '../containers/CharacterDesktopHeader' +import { CharacterMobileHeader } from '../containers/CharacterMobileHeader' + +interface CharacterPageProps { + type: 'character' | 'search' +} + +export const CharacterPage = ({ type }: CharacterPageProps) => { + const { characterStats, ability, hyperStat, basic, isLoading, error } = + useCharacterData() + const { inventory } = useInventory() + const [characterName, setCharacterName] = useState('') + const [showStats, setShowStats] = useState(true) + const [searchLoading, setSearchLoading] = useState(false) + const { setCharacterOcid } = useUserStore() + + const searchCharacterHandler = async () => { + if (characterName.trim() === '') { + alert('캐릭터 이름을 입력해주세요.') + return + } + setSearchLoading(true) + + try { + const { ocid } = await searchCharacterOcid(characterName.trim()) + if (!ocid) { + alert('캐릭터를 찾을 수 없습니다.') + return + } + setCharacterOcid(ocid) + } catch { + alert('캐릭터 검색에 실패했습니다.') + } finally { + setSearchLoading(false) + } + } + + // 에러 상태 + if (error) { + return + } + + return ( +
+ {/* 모바일 헤더 */} + + + {/* 데스크톱 헤더 */} + + + {/* 컨텐츠 */} + {isLoading ? ( + + ) : ( + + )} +
+ ) +} diff --git a/src/components/character/AbilitryContainer.tsx b/src/components/character/stats/AbilitryContainer.tsx similarity index 96% rename from src/components/character/AbilitryContainer.tsx rename to src/components/character/stats/AbilitryContainer.tsx index 98c911d..db18c1c 100644 --- a/src/components/character/AbilitryContainer.tsx +++ b/src/components/character/stats/AbilitryContainer.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { CharacterAbility } from '../../types/character' +import { CharacterAbility } from '../../../types/character' interface Props { ability: CharacterAbility diff --git a/src/components/character/DoubleStatRow.tsx b/src/components/character/stats/DoubleStatRow.tsx similarity index 100% rename from src/components/character/DoubleStatRow.tsx rename to src/components/character/stats/DoubleStatRow.tsx diff --git a/src/components/character/HyperStatContainer.tsx b/src/components/character/stats/HyperStatContainer.tsx similarity index 96% rename from src/components/character/HyperStatContainer.tsx rename to src/components/character/stats/HyperStatContainer.tsx index 11a8d6d..5e7bd65 100644 --- a/src/components/character/HyperStatContainer.tsx +++ b/src/components/character/stats/HyperStatContainer.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' -import { HyperStat } from '../../types/character' + import { HyperStatTable } from './HyperStatTable' +import { HyperStat } from '../../../types/character' interface Props { hyperStat: HyperStat diff --git a/src/components/character/HyperStatTable.tsx b/src/components/character/stats/HyperStatTable.tsx similarity index 93% rename from src/components/character/HyperStatTable.tsx rename to src/components/character/stats/HyperStatTable.tsx index 00408b3..0d1e8f6 100644 --- a/src/components/character/HyperStatTable.tsx +++ b/src/components/character/stats/HyperStatTable.tsx @@ -1,4 +1,4 @@ -import { HyperStatInfo } from '../../types/character' +import { HyperStatInfo } from '../../../types/character' interface Props { hyperStat: HyperStatInfo diff --git a/src/components/character/StatContainer.tsx b/src/components/character/stats/StatContainer.tsx similarity index 97% rename from src/components/character/StatContainer.tsx rename to src/components/character/stats/StatContainer.tsx index 0d26f61..3f777a5 100644 --- a/src/components/character/StatContainer.tsx +++ b/src/components/character/stats/StatContainer.tsx @@ -1,6 +1,6 @@ -import { CharacterStats } from '../../types/character' -import { formatKoreanNumber } from '../../utils/format' -import { getStatValue } from '../../utils/getStatValue' +import { CharacterStats } from '../../../types/character' +import { formatKoreanNumber } from '../../../utils/format' +import { getStatValue } from '../../../utils/getStatValue' import StatTable from './StatTable' interface Props { diff --git a/src/components/character/StatTable.tsx b/src/components/character/stats/StatTable.tsx similarity index 100% rename from src/components/character/StatTable.tsx rename to src/components/character/stats/StatTable.tsx diff --git a/src/components/guild/MemberContainer.tsx b/src/components/guild/MemberContainer.tsx index c3bfae7..f1a1330 100644 --- a/src/components/guild/MemberContainer.tsx +++ b/src/components/guild/MemberContainer.tsx @@ -33,16 +33,11 @@ export const MemberContainer = ({ setSearchCharacter }: MemberContainerProps) => { const [showMenu, setShowMenu] = useState(false) - const [isOpen, setIsOpen] = useState(false) - const [sortType, setSortType] = useState('캐릭터 정렬') const [sortTypeOpen, setSortTypeOpen] = useState(false) - const [selectedType, setSelectedType] = useState('캐릭터 분류') - const [gridSize, setGridSize] = useState(2) - const [showPart, setShowPart] = useState(false) // 분류해서 보기 const dropdownRef = useRef(null) @@ -125,17 +120,18 @@ export const MemberContainer = ({ }) return ( -
+
{guildName && ( -
-
+
+
+ {/* 길드 제목 부분 */}
-

+

길드: {guildName}

-
- -
+
+ +
본캐 @@ -164,15 +160,16 @@ export const MemberContainer = ({
-
-

- 총 인원 : {members.length} + +

+

+ 총 인원: {members.length}

-

- 본캐 : {members.filter(member => member.type === '본캐').length} +

+ 본캐: {members.filter(member => member.type === '본캐').length}

-

- 부캐 : +

+ 부캐: { members.filter( member => @@ -185,8 +182,8 @@ export const MemberContainer = ({ ).length }

-

- 외부 부캐 : +

+ 외부 부캐: { members.filter( member => @@ -201,21 +198,23 @@ export const MemberContainer = ({

+ + {/* 삭제 버튼 */} {onDeleteGuild && !isMainGuild && ( -
+
{showMenu && ( -
+
@@ -225,42 +224,45 @@ export const MemberContainer = ({
)} -
-
+
+ {/* 검색 및 필터 영역 */} +
+ {/* 검색창 */}
setSearchCharacter?.(e.target.value)} placeholder="캐릭터 이름으로 검색" - className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + className="w-full pl-9 sm:pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" /> - +
-
+
+ {/* 정렬 드롭다운 */}
{sortTypeOpen && ( -
+
)}
+ + {/* 분류 드롭다운 */}
{isOpen && ( -
- - - - - +
+ {['모두 보기', '본캐', '부캐', '외부 부캐', '특이사항'].map( + type => ( + + ) + )}
)}
+ {/* 그리드 크기 변경 */}
+ + {/* 분류해서 보기 토글 */}
-
+ + {/* 멤버 목록 */} +
{showPart ? ( (filteredMembers as Member[]).map(member => (
-
+ className="flex flex-col lg:flex-row gap-4 items-start border-b border-gray-100 pb-6 sm:pb-8"> + {/* 본캐 */} +
-
-
+ {/* 부캐들 */} +
+
{member.subCharacters?.map(subChar => ( + ? 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4' + : 'grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8' + }`}> {(filteredMembers as Member[]).map(member => ( { + const { + searchCharacterHandler, + characterName, + setCharacterName, + searchLoading + } = useCharacterSearch() + return ( +
+
+
+
+ +
+

캐릭터 검색

+
+
+ 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" + /> + +
+
+
+ ) +} + +export default CharacterSearchSection diff --git a/src/components/home/FeatureShowcase.tsx b/src/components/home/FeatureShowcase.tsx new file mode 100644 index 0000000..a810fc4 --- /dev/null +++ b/src/components/home/FeatureShowcase.tsx @@ -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 ( +
+
+
+ +
+

체험하기

+
+
+
+

비로그인 이용 가능한 기능

+
+ + 캐릭터 정보 조회 +
+
+ + 길드 정보 조회 +
+
+
+

+ 로그인 후 이용 가능한 기능 +

+
+ + 캘린더로 일정 관리하기 +
+
+ + 길드원 관리하기 +
+
+ +
+
+ ) +} + +export default FeatureShowcase diff --git a/src/components/home/GuildSearchSection.tsx b/src/components/home/GuildSearchSection.tsx new file mode 100644 index 0000000..71f1e43 --- /dev/null +++ b/src/components/home/GuildSearchSection.tsx @@ -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 ( +
+
+
+
+ +
+

+ 길드 검색 (최대 4개) +

+
+ +
+ {/* 서버 선택과 입력 필드 */} +
+ + +
+ 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" + /> + +
+
+ + {/* 길드 목록 */} + {guildList.length > 0 && ( +
+

검색할 길드 목록

+
+ {guildList.map(guild => ( +
+ {guild} + +
+ ))} +
+
+ )} + + +
+
+
+ ) +} + +export default GuildSearchSection diff --git a/src/components/home/HeroSection.tsx b/src/components/home/HeroSection.tsx new file mode 100644 index 0000000..340ca48 --- /dev/null +++ b/src/components/home/HeroSection.tsx @@ -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 ( +
+
+ 메이플링크 로고 +
+ + + 메이플스토리 통합 관리 플랫폼 + +
+
+

+ 메이플스토리를 +
더 스마트하게 +

+

+ 캐릭터부터 길드까지, 한눈에 관리하세요 +

+
+ + +
+
+ ) +} + +export default HeroSection diff --git a/src/components/home/HomeNavigation.tsx b/src/components/home/HomeNavigation.tsx new file mode 100644 index 0000000..f65de22 --- /dev/null +++ b/src/components/home/HomeNavigation.tsx @@ -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 ( + + ) +} + +export default HomeNavigation diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index ebf2ee6..7b1949b 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -38,9 +38,9 @@ const Layout = ({ children, hide }: LayoutProps) => { } return ( -
+
{!hide &&
} -
+
{children}
{!hide &&