diff --git a/src/apis/user/userController.ts b/src/apis/user/userController.ts index 0b56ced..ccb4383 100644 --- a/src/apis/user/userController.ts +++ b/src/apis/user/userController.ts @@ -30,3 +30,13 @@ export const addUserInfo = async (apiKey: string) => { throw error } } + +export const syncCharacter = async () => { + try { + const response = await basicApi.post(`/api/game-character/sync-character`) + return response.data + } catch (error) { + console.error('Error syncing character:', error) + throw error + } +} diff --git a/src/components/character/CharacterPage.tsx b/src/components/character/CharacterPage.tsx index 88ef241..66e8d89 100644 --- a/src/components/character/CharacterPage.tsx +++ b/src/components/character/CharacterPage.tsx @@ -6,11 +6,18 @@ import { InventoryContainer } from './inventory/InventoryContainer' import { StatContainer } from './StatContainer' import { useCharacterData } from '../../hooks/character/useCharacterData' import { useInventory } from '../../hooks/character/useInventory' -import { useNavigate } from 'react-router-dom' + import { useUserStore } from '../../store/userStore' import { searchCharacterOcid } from '../../apis/character/characterController' -export const CharacterPage = () => { +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('') @@ -18,7 +25,7 @@ export const CharacterPage = () => { const { inventory } = useInventory() const [showStats, setShowStats] = useState(true) - const nav = useNavigate() + const { setCharacterOcid } = useUserStore() const [searchLoading, setSearchLoading] = useState(false) @@ -36,39 +43,168 @@ export const CharacterPage = () => { return } setCharacterOcid(ocid) - } catch (error) { - console.error(error) + } catch { alert('캐릭터 검색에 실패했습니다.') } finally { setSearchLoading(false) } } - if (isLoading) { + 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" + /> + +
+
+
) } - if (error) { - alert( - '2023년 12월 21일 이후의 데이터만 조회할 수 있습니다.\n게임에 접속한 뒤 다시 시도해주세요.' - ) - nav('/') - } - return (
{/* 탭 버튼 */}
-
+ {type === 'character' ? ( +
+
+ {/* */} +
+
+
+
+

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

+

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

+
+
+
+
+
+ ) : ( +
+ )}
{/* 콘텐츠 영역 */} -
- {showStats ? ( -
- {/* 캐릭터 정보와 어빌리티 (큰 화면에서 세로로 배치) */} -
- {/* 캐릭터 정보 (작은 화면에서 맨 위) */} -
{basic && }
- - {/* 어빌리티 (캐릭터 정보 아래에 배치) */} -
{ability && }
-
+ {isLoading ? ( +
+ {showStats ? ( +
+ {/* 캐릭터 정보와 어빌리티 스켈레톤 */} +
+
+
+
+ + {/* 기본 스탯 스켈레톤 */} +
+
+
- {/* 기본 스탯 (큰 화면에서 중앙에 위치) */} -
- {characterStats && } + {/* 하이퍼 스탯 스켈레톤 */} +
+
+
+ ) : ( +
+ {/* 캐릭터 이미지 스켈레톤 */} +
+ {/* 장비 정보 스켈레톤 */} +
+ {Array.from({ length: 15 }).map((_, index) => ( +
+ ))} +
+
+ )} +
+ ) : ( +
+ {showStats ? ( +
+ {/* 캐릭터 정보와 어빌리티 (큰 화면에서 세로로 배치) */} +
+ {/* 캐릭터 정보 (작은 화면에서 맨 위) */} +
{basic && }
+ + {/* 어빌리티 (캐릭터 정보 아래에 배치) */} +
{ability && }
+
+ + {/* 기본 스탯 (큰 화면에서 중앙에 위치) */} +
+ {characterStats && } +
- {/* 하이퍼 스탯 (큰 화면에서 오른쪽에 위치) */} -
- {hyperStat && } + {/* 하이퍼 스탯 (큰 화면에서 오른쪽에 위치) */} +
+ {hyperStat && } +
-
- ) : ( -
- {inventory && basic?.character_image && ( - - )} -
- )} -
+ ) : ( +
+ {inventory && basic?.character_image && ( + + )} +
+ )} +
+ )}
) } diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index 8a00f9f..6d4f307 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -5,6 +5,7 @@ import Logo from '../../assets/logo.png' import { useState } from 'react' import { useAuthStore } from '../../store/authStore' import KakaoOpenChatButton from './KakaoOpenChatButton' +import { useUserStore } from '../../store/userStore' function Header() { const { userLogout } = useAuth() @@ -12,6 +13,7 @@ function Header() { const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const { userType } = useAuthStore() + const { setCharacterOcid, userInfo } = useUserStore() const KAKAO_CHAT_LINK = 'https://open.kakao.com/o/s4tfG2Ah' @@ -59,7 +61,10 @@ function Header() { diff --git a/src/components/modal/room/RoomManageModal.tsx b/src/components/modal/room/RoomManageModal.tsx index 2c9388a..625b755 100644 --- a/src/components/modal/room/RoomManageModal.tsx +++ b/src/components/modal/room/RoomManageModal.tsx @@ -1,5 +1,4 @@ import { Room } from '../../../types/rooms' -import InputText from '../../common/InputText' import ModalLayout from '../ModalLayout' import { useAdmin } from '../../../hooks/room/useAdmin' import { useState } from 'react' @@ -76,80 +75,196 @@ export const GuildManageModal = ({ size="medium" title={`${room.groupName} 관리`} showFooterButtons={false}> -
-
-
-

관리자 목록

-
- { - setCharacterName(e.target.value) - // 입력 시작하면 이전 메시지 제거 - setMessage(null) - }} - /> +
+
+ {/* 안내 메시지 */} +
+
+
+ +
+
+

+ 관리자 초대 시 주의사항 +

+

+ 관리자로 초대할 때는 반드시 해당 길드원의{' '} + 본캐릭터를 등록해야 합니다. +
+ 본캐릭터는 넥슨 OpenAPI에서 레벨이 가장 높은 캐릭터를 기준으로 + 자동 설정됩니다. +

+
+
+
+ + {/* 관리자 추가 폼 */} +
+
+

+ 관리자 추가 +

+
+
+
+
+ + + + +
+ { + setCharacterName(e.target.value) + setMessage(null) + }} + className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" + /> +
+ {/* 메시지 표시 */} {message && (
- {message.text} + {message.text}
)} -
-
-
닉네임
-
권한
+ {/* 관리자 목록 */} +
+
+

+ 관리자 목록 +

- {room.admins.map((admin, index) => ( -
-
{admin}
-
- {room.admins.length > 0 && - room.admins[index] !== userName && ( +
+
+ {room.admins.map(admin => ( +
+
+
+ + + + +
+
+ + {admin} + + {admin === userName && ( + + 나 + + )} +
+
+ {admin !== userName && ( )} -
+
+ ))}
- ))} +
diff --git a/src/hooks/character/useCharacterData.ts b/src/hooks/character/useCharacterData.ts index 6f8cac6..49a6416 100644 --- a/src/hooks/character/useCharacterData.ts +++ b/src/hooks/character/useCharacterData.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' import { fetchCharacterAbility, fetchCharacterBasic, @@ -14,10 +14,14 @@ import { import { useUserStore } from '../../store/userStore' import { useEffect } from 'react' +import { syncCharacter } from '../../apis/user/userController' +import { useAuth } from '../useAuth' +import { useNavigate } from 'react-router-dom' export const useCharacterData = () => { const { setUserName, characterOcid } = useUserStore() - + const { userLogout } = useAuth() + const nav = useNavigate() const { data: characterStats, isLoading: statsLoading, @@ -70,6 +74,28 @@ export const useCharacterData = () => { retry: false }) + const mutateSyncCharacter = useMutation({ + mutationFn: () => syncCharacter(), + onSuccess: () => { + alert('동기화가 완료되었습니다. 다시 로그인해주세요.') + userLogout() + nav('/') + }, + onError: () => { + alert('동기화에 실패했습니다. 다시 시도해주세요.') + } + }) + + const syncCharacterHandler = async () => { + if (mutateSyncCharacter.isPending) return + + try { + await mutateSyncCharacter.mutateAsync() + } catch { + alert('동기화에 실패했습니다. 다시 시도해주세요.') + } + } + useEffect(() => { if (basic?.character_name) { setUserName(basic.character_name) @@ -87,6 +113,8 @@ export const useCharacterData = () => { hyperStat, basic, isLoading, - error + error, + syncCharacterHandler, + isSyncing: mutateSyncCharacter.isPending } } diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index c47e7bb..7dd1b7c 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -10,17 +10,23 @@ export const useAuth = () => { const userLogin = async () => { try { - const provider = new GoogleAuthProvider() + const provider = await new GoogleAuthProvider() const result = await signInWithPopup(authService, provider) const token = await result.user.getIdToken() - if (token && result.user.uid) { - storeLogin(token, result.user.uid, 'member') - const userInfo = await fetchUserInfo(result.user.uid) - return userInfo + if (!token || !result.user.uid) { + throw new Error('로그인에 필요한 정보를 가져올 수 없습니다.') } - } catch (error) { - console.log(error) + + storeLogin(token, result.user.uid, 'member') + const userInfo = await fetchUserInfo(result.user.uid) + + if (!userInfo) { + throw new Error('사용자 정보를 가져올 수 없습니다.') + } + return userInfo + } catch { + return null } } diff --git a/src/pages/Character.tsx b/src/pages/Character.tsx index 743c65c..1ec2480 100644 --- a/src/pages/Character.tsx +++ b/src/pages/Character.tsx @@ -1,7 +1,7 @@ import { CharacterPage } from '../components/character/CharacterPage' const Character = () => { - return + return } export default Character diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index a38ae8f..0a2c66c 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -60,8 +60,7 @@ const Home = () => { nav('/signup') } } - } catch (error) { - console.error(error) + } catch { alert('로그인에 실패했습니다.') } } @@ -83,8 +82,7 @@ const Home = () => { await storeLogin('', '', 'search') setCharacterOcid(ocid) nav(`/searchCharacter`) - } catch (error) { - console.error(error) + } catch { alert('캐릭터 검색에 실패했습니다.') } finally { setSearchLoading(false) diff --git a/src/pages/SearchCharacter.tsx b/src/pages/SearchCharacter.tsx index 9ca9202..b0a49b9 100644 --- a/src/pages/SearchCharacter.tsx +++ b/src/pages/SearchCharacter.tsx @@ -1,5 +1,5 @@ import { CharacterPage } from '../components/character/CharacterPage' export const SearchCharacter = () => { - return + return }