diff --git a/src/App.tsx b/src/App.tsx index b1be2f4..da0e0a9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import Room from './pages/Room' import { GuildPromotion } from './pages/GuildPromotion' import Notice from './pages/Notice' import { SearchCharacter } from './pages/SearchCharacter' +import { SearchGuild } from './pages/SearchGuild' const router = createBrowserRouter([ { @@ -126,6 +127,14 @@ const router = createBrowserRouter([ ) + }, + { + path: '/searchGuild', + element: ( + + + + ) } ]) diff --git a/src/apis/guild/guildController.ts b/src/apis/guild/guildController.ts index c631603..7b2213b 100644 --- a/src/apis/guild/guildController.ts +++ b/src/apis/guild/guildController.ts @@ -1,5 +1,10 @@ -import { basicApi, nexonApi } from '..' -import { Guild, SearchGuild } from '../../types/guild' +import { basicApi, nexonApi, publicApi } from '..' +import { + Guild, + SearchGuild, + SearchGuildMemberResponse, + SearchGuildResponse +} from '../../types/guild' //길드 목록 조회 export const fetchGuildList = async () => { @@ -48,3 +53,30 @@ export const searchGuild = async (params: Guild) => { ) return guildInfo } + +//로그인 없이 길드 조회 +export const searchGuildWithoutLogin = async ( + guildNames: string[], + worldName: string +) => { + const response = await publicApi.post( + '/api/v1/public/guilds/async/test', + { + guildNames, + worldName + } + ) + return response.data +} + +//로그인 없이 길드 본/부캐 조회 + +export const searchGuildMemberWithoutLogin = async (members: string[]) => { + const response = await publicApi.post( + '/api/v1/public/guilds/member/async', + { + members + } + ) + return response.data +} diff --git a/src/apis/index.ts b/src/apis/index.ts index b24ae6a..302503d 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -14,6 +14,14 @@ export const basicApi = axios.create({ } }) +export const publicApi = axios.create({ + baseURL: API_KEY, + timeout: DEFAULT_TIMEOUT, + headers: { + 'Content-Type': 'application/json;charset=utf-8' + } +}) + basicApi.interceptors.request.use(config => { const token = useAuthStore.getState().token if (token) { diff --git a/src/components/character/CharacterPage.tsx b/src/components/character/CharacterPage.tsx index e7e93ae..dddecc9 100644 --- a/src/components/character/CharacterPage.tsx +++ b/src/components/character/CharacterPage.tsx @@ -7,26 +7,38 @@ 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' -interface CharacterPageProps { - type: 'character' | 'search' - characterName?: string - setCharacterName?: (characterName: string) => void - searchCharacterHandler?: () => Promise -} -export const CharacterPage = ({ - type, - characterName, - setCharacterName, - searchCharacterHandler -}: CharacterPageProps) => { +export const CharacterPage = () => { const { characterStats, ability, hyperStat, basic, isLoading, error } = useCharacterData() + const [characterName, setCharacterName] = useState('') const { inventory } = useInventory() const [showStats, setShowStats] = useState(true) const nav = useNavigate() + const { setCharacterOcid } = useUserStore() + const [searchLoading, setSearchLoading] = useState(false) + + const searchCharacterHandler = async () => { + if (characterName.trim() === '') { + alert('캐릭터 이름을 입력해주세요.') + return + } + + setSearchLoading(true) + const { ocid } = await searchCharacterOcid(characterName.trim()) + + if (!ocid) { + alert('캐릭터를 찾을 수 없습니다.') + return + } + + setCharacterOcid(ocid) + setSearchLoading(false) + } if (isLoading) { return ( @@ -73,24 +85,26 @@ export const CharacterPage = ({ 장비 정보 - {type === 'search' ? ( -
- setCharacterName?.(e.target.value)} - /> - -
- ) : ( -
{/* 오른쪽 여백을 위한 빈 div */}
- )} + +
+ setCharacterName?.(e.target.value)} + /> + +
{/* 콘텐츠 영역 */} diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index 45a36a9..fb3112f 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -23,7 +23,9 @@ function Header() {
- {userType === 'search' ? ( + {userType === 'search' || userType === undefined ? ( <> - {/* */} + ) : ( <> @@ -131,7 +133,7 @@ function Header() { {/* Mobile menu */} {mobileMenuOpen && (
- {userType === 'search' ? ( + {userType === 'search' || userType === undefined ? ( <> - {/* */} + ) : ( <> diff --git a/src/components/guild/ActionBtnList.tsx b/src/components/guild/ActionBtnList.tsx index cb7cb57..f9ccac0 100644 --- a/src/components/guild/ActionBtnList.tsx +++ b/src/components/guild/ActionBtnList.tsx @@ -11,17 +11,21 @@ import { } from 'react-icons/io5' interface Props { - showModal: (name: ModalType) => void + showModal?: (name: ModalType) => void guildList: Guild[] - handleDetect: () => void + handleDetect?: () => void refreshMember?: (guildId: number) => void + mainCharacterInfoSearchHandler?: () => void + isUpdating?: boolean } export const ActionBtnList = ({ showModal, guildList, handleDetect, - refreshMember + refreshMember, + mainCharacterInfoSearchHandler, + isUpdating }: Props) => { const [searchParams, setSearchParams] = useSearchParams() const [selectedGuild, setSelectedGuild] = useState(null) @@ -87,104 +91,154 @@ export const ActionBtnList = ({ return (
- -
-
- ⚠️ 관리방 생성은 길드 마스터만 가능합니다. -
마스터가 아니라면, 마스터를 그룹에 초대해 생성해주세요. -
-
+ {showModal && ( + <> + +
+
+ ⚠️ 관리방 생성은 길드 마스터만 가능합니다. +
마스터가 아니라면, 마스터를 그룹에 초대해 생성해주세요. +
+
+ + )}
{guildList.length > 0 && ( <>
-
- + {showModal ? ( +
+ - {isOpen && ( -
+ {isOpen && ( +
+ {guildList.map(guild => ( + + ))} +
+ )} +
+ ) : ( +
+
{guildList.map(guild => ( ))}
- )} -
+ +
+ )} -
-
- { + {handleDetect && showModal && ( +
+
- } - -
-
- 캐릭터의 본/부캐 정보를 새로고침합니다 +
+
+ 기록된 길드원 정보와 게임 내 정보를 비교하고 변경합니다. +
+ ⭐︎ 길드원 변동이 있을경우 꼭 눌러주세요. ⭐︎ +
-
+
+ { + + } -
- -
-
- 기록된 길드원 정보를 게임 내 정보와 비교합니다. +
+
+ 캐릭터의 본/부캐 정보가 오류 있을 경우 새로고침합니다. +
-
+ )}
)} diff --git a/src/components/guild/MemberContainer.tsx b/src/components/guild/MemberContainer.tsx index cdbf05a..f61fe83 100644 --- a/src/components/guild/MemberContainer.tsx +++ b/src/components/guild/MemberContainer.tsx @@ -4,7 +4,8 @@ import { IoChevronDown, IoEllipsisVertical, IoSearchOutline, - IoHelpCircleOutline + IoHelpCircleOutline, + IoGridOutline } from 'react-icons/io5' interface MemberContainerProps { @@ -34,8 +35,13 @@ export const MemberContainer = ({ const [isOpen, setIsOpen] = useState(false) + const [sortType, setSortType] = useState('캐릭터 정렬') + const [sortTypeOpen, setSortTypeOpen] = useState(false) + const [selectedType, setSelectedType] = useState('캐릭터 분류') + const [gridSize, setGridSize] = useState(2) + const dropdownRef = useRef(null) if (!members) return null @@ -48,77 +54,126 @@ export const MemberContainer = ({ setShowMenu(false) } - const filteredMembers = members.filter(member => { - if (selectedType === '모두 보기' || selectedType === '캐릭터 분류') - return member.name + const filteredMembers = members + .filter(member => { + const searchMatch = member.name .toLowerCase() .includes(searchCharacter?.toLowerCase() || '') - if (selectedType === '본캐') - return ( - member.type === '본캐' && - member.name.toLowerCase().includes(searchCharacter?.toLowerCase() || '') - ) - if (selectedType === '부캐') + + if (selectedType === '모두 보기' || selectedType === '캐릭터 분류') + return searchMatch + + if (selectedType === '본캐') return member.type === '본캐' && searchMatch + + if (selectedType === '부캐') + return ( + member.type === '부캐' && + allMembers?.find(m => + m.memberDetailResponse?.find( + m => m.name === member.mainCharacterInfo?.name + ) + ) && + searchMatch + ) + + if (selectedType === '특이사항') return member.description && searchMatch + return ( member.type === '부캐' && - allMembers?.find(m => + !allMembers?.find(m => m.memberDetailResponse?.find( - m => m.id === member.mainCharacterInfo?.id + m => m.name === member.mainCharacterInfo?.name ) ) && - member.name.toLowerCase().includes(searchCharacter?.toLowerCase() || '') - ) - if (selectedType === '특이사항') - return ( - member.description && - member.name.toLowerCase().includes(searchCharacter?.toLowerCase() || '') - ) - return ( - member.type === '부캐' && - !allMembers?.find(m => - m.memberDetailResponse?.find(m => m.id === member.mainCharacterInfo?.id) + searchMatch ) - ) - }) + }) + .sort((a, b) => { + if (sortType === '이름순') { + return a.name.localeCompare(b.name) + } + if (sortType === '레벨순') { + return Number(b.level) - Number(a.level) + } + return 0 + }) return ( -
+
{guildName && (
-
-

- 길드: {guildName} -

-
- -
-
- - 본캐 - -

- 길드의 메인 캐릭터입니다. -

-
-
- - 부캐 - -

- 같은 길드 내 본캐가 있는 부캐릭터입니다. -

-
-
- - 외부 부캐 - -

- 다른 길드에 본캐가 있는 부캐릭터입니다. -

+
+
+

+ 길드: {guildName} +

+
+ +
+
+ + 본캐 + +

+ 길드의 메인 캐릭터입니다. +

+
+
+ + 부캐 + +

+ 같은 길드 내 본캐가 있는 부캐릭터입니다. +

+
+
+ + 외부 부캐 + +

+ 다른 길드에 본캐가 있는 부캐릭터입니다. +

+
+
-
+
+

+ 총 인원 : {members.length} +

+

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

+

+ 부캐 : + { + members.filter( + member => + member.type === '부캐' && + allMembers?.find(m => + m.memberDetailResponse?.find( + m => m.name === member.mainCharacterInfo?.name + ) + ) + ).length + } +

+

+ 외부 부캐 : + { + members.filter( + member => + member.type === '부캐' && + !allMembers?.some(m => + m.memberDetailResponse?.some( + m => m.name === member.mainCharacterInfo?.name + ) + ) + ).length + } +

+
{onDeleteGuild && !isMainGuild && (
@@ -145,7 +200,7 @@ export const MemberContainer = ({ )}
-
+
-
- + + {sortTypeOpen && ( +
+ + +
)} +
+
+ - - + {isOpen && ( +
+ + + + + +
+ )} +
- {isOpen && ( -
- - - - - -
- )} +
+ +
-
+
{filteredMembers.map(member => (
g.memberDetailResponse) .find( m => - m?.id === member.mainCharacterInfo!.id && + m?.name === member.mainCharacterInfo!.name && m.type === '본캐' ) @@ -246,58 +353,83 @@ export const MemberContainer = ({ : () => onSelect?.(member.type, member) } className="bg-gray-50 rounded-lg hover:bg-gray-100 transition-all duration-200 cursor-pointer group overflow-hidden border border-gray-100"> -
+
{member.name} -
+

{member.name}

{member.job}

-
-
-
+
+
+
Lv.{member.level} {masterName && ( + m.memberDetailResponse?.find( + m => + m.name === + member.mainCharacterInfo?.name + ) + ) + ? 'bg-yellow-100 text-yellow-700' + : 'bg-gray-100 text-gray-700' + }`}> + {member.name === masterName && ( + + + + )} + {member.name === masterName + ? '마스터' + : member.type === '본캐' + ? '본캐' : member.type === '부캐' && allMembers?.find(m => m.memberDetailResponse?.find( m => - m.id === member.mainCharacterInfo?.id + m.name === + member.mainCharacterInfo?.name ) ) - ? 'bg-yellow-100 text-yellow-700' - : 'bg-gray-100 text-gray-700' - }`}> - {member.type === '본캐' - ? '본캐' - : member.type === '부캐' && - allMembers?.find(m => - m.memberDetailResponse?.find( - m => m.id === member.mainCharacterInfo?.id - ) - ) - ? '부캐' - : '외부 부캐'} + ? '부캐' + : member.mainCharacterInfo === null + ? '' + : '외부 부캐'} )}
{member.type === '부캐' && !allMembers?.find(m => m.memberDetailResponse?.find( - m => m.id === member.mainCharacterInfo?.id + m => m.name === member.mainCharacterInfo?.name ) ) && ( - + 외부 길드에 {member.mainCharacterInfo?.name}님의 부캐입니다. diff --git a/src/components/modal/guild/DetailMemberModal.tsx b/src/components/modal/guild/DetailMemberModal.tsx index 1c07e09..203c489 100644 --- a/src/components/modal/guild/DetailMemberModal.tsx +++ b/src/components/modal/guild/DetailMemberModal.tsx @@ -21,13 +21,16 @@ export const DetailMemberModal = ({ }: Props) => { const [isEditMode, setIsEditMode] = useState(false) const [description, setDescription] = useState(memberDetail.description || '') - const [selectedTab, setSelectedTab] = useState<'info' | 'alts'>('info') + const [selectedTab, setSelectedTab] = useState<'info' | 'alts'>( + memberDetail.description ? 'info' : 'alts' + ) const subCharacterList = memberList.flatMap(n => n.memberDetailResponse ?.filter( - m => m.type === '부캐' && m.mainCharacterInfo?.id === memberDetail.id + m => + m.type === '부캐' && m.mainCharacterInfo?.name === memberDetail.name ) .map(m => ({ ...m, @@ -111,6 +114,9 @@ export const DetailMemberModal = ({

부캐릭터 목록

+

+ 부캐릭터 총 {subCharacterList.length}개 +

{subCharacterList.map(alt => ( diff --git a/src/data/worlds.ts b/src/data/worlds.ts index 1014364..e61decf 100644 --- a/src/data/worlds.ts +++ b/src/data/worlds.ts @@ -30,3 +30,20 @@ export const worldNames: WorldData[] = [ { name: '에오스', icon: eosIcon }, { name: '핼리오스', icon: heliosIcon } ] + +export const servers = [ + { id: '스카니아', name: '스카니아' }, + { id: '베라', name: '베라' }, + { id: '루나', name: '루나' }, + { id: '제니스', name: '제니스' }, + { id: '크로아', name: '크로아' }, + { id: '유니온', name: '유니온' }, + { id: '엘리시움', name: '엘리시움' }, + { id: '이노시스', name: '이노시스' }, + { id: '레드', name: '레드' }, + { id: '오로라', name: '오로라' }, + { id: '아케인', name: '아케인' }, + { id: '노바', name: '노바' }, + { id: '에오스', name: '에오스' }, + { id: '핼리오스', name: '핼리오스' } +] diff --git a/src/hooks/calendar/useUserNotice.ts b/src/hooks/calendar/useUserNotice.ts index d64e400..318a46b 100644 --- a/src/hooks/calendar/useUserNotice.ts +++ b/src/hooks/calendar/useUserNotice.ts @@ -20,7 +20,7 @@ export const useUserNotice = () => { const { data, isLoading } = useQuery({ queryKey: ['calendar'], queryFn: getCalendar, - enabled: userType !== 'guest' + enabled: userType === 'member' }) const createPersonalCalendarMutation = useMutation({ diff --git a/src/hooks/character/useCharacterData.ts b/src/hooks/character/useCharacterData.ts index 629f5f6..6f8cac6 100644 --- a/src/hooks/character/useCharacterData.ts +++ b/src/hooks/character/useCharacterData.ts @@ -16,16 +16,17 @@ import { useUserStore } from '../../store/userStore' import { useEffect } from 'react' export const useCharacterData = () => { - const { userInfo, setUserName } = useUserStore() - const ocid = userInfo?.ocid + const { setUserName, characterOcid } = useUserStore() const { data: characterStats, isLoading: statsLoading, error: statsError } = useQuery({ - queryKey: ['characterStats', ocid], - queryFn: ocid ? () => fetchCharacterStat(ocid) : undefined, + queryKey: ['characterStats', characterOcid], + queryFn: characterOcid + ? () => fetchCharacterStat(characterOcid) + : undefined, staleTime: 5 * 60 * 1000, retry: false }) @@ -35,8 +36,10 @@ export const useCharacterData = () => { isLoading: abilityLoading, error: abilityError } = useQuery({ - queryKey: ['characterAbility', ocid], - queryFn: ocid ? () => fetchCharacterAbility(ocid) : undefined, + queryKey: ['characterAbility', characterOcid], + queryFn: characterOcid + ? () => fetchCharacterAbility(characterOcid) + : undefined, staleTime: 5 * 60 * 1000, retry: false }) @@ -46,8 +49,10 @@ export const useCharacterData = () => { isLoading: hyperLoading, error: hyperError } = useQuery({ - queryKey: ['characterHyperStat', ocid], - queryFn: ocid ? () => fetchCharacterHyperStat(ocid) : undefined, + queryKey: ['characterHyperStat', characterOcid], + queryFn: characterOcid + ? () => fetchCharacterHyperStat(characterOcid) + : undefined, staleTime: 5 * 60 * 1000, retry: false }) @@ -57,8 +62,10 @@ export const useCharacterData = () => { isLoading: basicLoading, error: basicError } = useQuery({ - queryKey: ['characterBasic', ocid], - queryFn: ocid ? () => fetchCharacterBasic(ocid) : undefined, + queryKey: ['characterBasic', characterOcid], + queryFn: characterOcid + ? () => fetchCharacterBasic(characterOcid) + : undefined, staleTime: 5 * 60 * 1000, retry: false }) diff --git a/src/hooks/character/useInventory.ts b/src/hooks/character/useInventory.ts index 064bd64..40f3cfb 100644 --- a/src/hooks/character/useInventory.ts +++ b/src/hooks/character/useInventory.ts @@ -4,12 +4,12 @@ import { fetchCharacterItem } from '../../apis/character/characterController' import { Inventory } from '../../types/item' export const useInventory = () => { - const { userInfo } = useUserStore() + const { characterOcid } = useUserStore() const { data: inventory, isLoading: inventoryLoading } = useQuery({ - queryKey: ['inventory', userInfo?.ocid], - queryFn: () => fetchCharacterItem(userInfo!.ocid!), + queryKey: ['inventory', characterOcid], + queryFn: () => fetchCharacterItem(characterOcid!), staleTime: 5 * 60 * 1000, - enabled: !!userInfo?.ocid + enabled: !!characterOcid }) return { inventory, inventoryLoading } diff --git a/src/hooks/guild/useGuildMember.ts b/src/hooks/guild/useGuildMember.ts index ab3b1ea..629134d 100644 --- a/src/hooks/guild/useGuildMember.ts +++ b/src/hooks/guild/useGuildMember.ts @@ -28,7 +28,7 @@ export const useGuildMember = () => { queryFn: () => Promise.all(guildList?.map(v => fetchGuildMembers(v.guildId ?? 0)) || []), staleTime: 1000 * 60 * 10, - enabled: userType !== 'guest' + enabled: userType === 'member' }) if (userType === 'guest') { diff --git a/src/hooks/search/useSearchGuild.ts b/src/hooks/search/useSearchGuild.ts new file mode 100644 index 0000000..b95a7e5 --- /dev/null +++ b/src/hooks/search/useSearchGuild.ts @@ -0,0 +1,155 @@ +import { + searchGuildMemberWithoutLogin, + searchGuildWithoutLogin +} from '../../apis/guild/guildController' +import { useState } from 'react' + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useSearchParams } from 'react-router-dom' +import { SearchGuildResponse } from '../../types/guild' + +export const useSearchGuild = () => { + const [guildList, setGuildList] = useState([]) + const [selectedServer, setSelectedServer] = useState('') + const [guildName, setGuildName] = useState('') + const [searchParams, setSearchParams] = useSearchParams() + const [isUpdating, setIsUpdating] = useState(false) + + const params = new URLSearchParams(searchParams) + + const serachGuildList = params.get('guildList')?.split(',') || [] + const serachServer = params.get('server') || '' + const selectedGuild = params.get('guild') || '' + + const queryClient = useQueryClient() + + const { + data: guildsInfo, + isLoading, + isError + } = useQuery({ + queryKey: ['guildsInfo', serachGuildList, serachServer], + queryFn: () => searchGuildWithoutLogin(serachGuildList, serachServer), + retry: false, + staleTime: 1000 * 60 * 10 + }) + + const mainCharacterInfoSearchMutation = useMutation({ + mutationFn: (members: string[]) => searchGuildMemberWithoutLogin(members) + }) + + const mainCharacterInfoSearchHandler = async () => { + if (!guildsInfo) return + setIsUpdating(true) + + try { + const allMemberNames = guildsInfo + .map(guild => guild.guildMember.map(member => member.name)) + .flat() + + const response = + await mainCharacterInfoSearchMutation.mutateAsync(allMemberNames) + + queryClient.setQueryData( + ['guildsInfo', serachGuildList, serachServer], + (oldData: SearchGuildResponse[]) => { + if (!oldData) return oldData + + return oldData.map(guild => ({ + ...guild, + guildMember: guild.guildMember.map(member => { + const match = response.find(res => res.memberName === member.name) + const matchedMember = match?.mainCharacterInfo + + return { + ...member, + type: match?.type ?? member.type, + mainCharacterInfo: matchedMember ?? member.mainCharacterInfo + } + }) + })) + } + ) + } catch (error) { + console.error('메인 캐릭터 정보 조회 중 오류 발생:', error) + alert('메인 캐릭터 정보 조회 중 오류가 발생했습니다.') + } finally { + setIsUpdating(false) + } + } + + const addGuildList = (guildName: string) => { + if (!selectedServer) { + alert('서버를 선택해주세요.') + return + } + if (guildList.includes(guildName)) { + alert('이미 추가된 길드입니다.') + return + } + if (guildName.trim() === '') { + alert('길드 이름을 입력해주세요.') + return + } + setGuildList(prev => [...prev, guildName]) + setGuildName('') + } + + const removeGuildList = (guildToRemove: string) => { + setGuildList(prev => prev.filter(guild => guild !== guildToRemove)) + } + + const handleGuildKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + addGuildList(guildName) + } + } + const searchGuildHandler = async () => { + if (guildList.length === 0) { + alert('검색할 길드를 추가해주세요.') + return + } + + if (!selectedServer) { + alert('서버를 선택해주세요.') + return + } + + const newSearchParams = new URLSearchParams(searchParams) + newSearchParams.set('guildList', guildList.join(',')) + newSearchParams.set('server', selectedServer) + setSearchParams(newSearchParams) + } + + const selectedGuildMember = guildsInfo?.find( + guild => guild.guildName === selectedGuild + ) + + const resetSearchParams = () => { + setSearchParams(new URLSearchParams()) + } + + if (isError) { + alert('길드 정보가 존재하지 않습니다.') + resetSearchParams() + } + + return { + searchGuildHandler, + guildList, + setGuildList, + selectedServer, + setSelectedServer, + guildName, + setGuildName, + guildsInfo, + isLoading, + isUpdating, + addGuildList, + removeGuildList, + handleGuildKeyPress, + selectedGuildMember, + mainCharacterInfoSearchHandler, + resetSearchParams + } +} diff --git a/src/pages/Character.tsx b/src/pages/Character.tsx index 1ec2480..743c65c 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 0baa811..326bfd2 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -12,30 +12,39 @@ import { import Logo from '../assets/logo.png' import GoogleLogo from '../assets/gogle.svg' import { useUserStore } from '../store/userStore' -import { guest } from '../data/guest' import { useState } from 'react' import Button from '../components/common/Button' import { searchCharacterOcid } from '../apis/character/characterController' +import { useSearchGuild } from '../hooks/search/useSearchGuild' +import { servers } from '../data/worlds' +import { guest } from '../data/guest' const Home = () => { const { userLogin } = useAuth() const { storeLogin } = useAuthStore() - const { setUserInfo } = useUserStore() + const { setUserInfo, setCharacterOcid } = useUserStore() + const [searchLoading, setSearchLoading] = useState(false) const nav = useNavigate() const KAKAO_CHAT_LINK = 'https://open.kakao.com/o/s4tfG2Ah' const [characterName, setCharacterName] = useState('') - const [guildName, setGuildName] = useState('') + + const { + selectedServer, + setSelectedServer, + guildList, + searchGuildHandler, + addGuildList, + removeGuildList, + handleGuildKeyPress, + guildName, + setGuildName + } = useSearchGuild() const handleGuestLogin = async () => { await storeLogin('', '', 'guest') - setUserInfo({ - id: 0, - firebaseId: '1', - name: 'guest', - email: 'play3step@gmail.com', - ocid: guest.ocid - }) + setCharacterOcid(guest.ocid) + nav('/character') } @@ -44,6 +53,7 @@ const Home = () => { const userInfo = await userLogin() if (userInfo) { setUserInfo(userInfo) + setCharacterOcid(userInfo.ocid!) if (userInfo?.nexonApiKey) { nav('/character') } else { @@ -62,6 +72,7 @@ const Home = () => { return } + setSearchLoading(true) const { ocid } = await searchCharacterOcid(characterName.trim()) if (!ocid) { @@ -70,15 +81,14 @@ const Home = () => { } await storeLogin('', '', 'search') - - setUserInfo({ - id: 0, - firebaseId: '1', - name: characterName.trim(), - email: 'play3step@gmail.com', - ocid: ocid - }) + setCharacterOcid(ocid) nav(`/searchCharacter`) + setSearchLoading(false) + } + + const onSearchGuild = async () => { + nav(`/searchGuild`) + searchGuildHandler() } const recentNotices = [ @@ -96,6 +106,12 @@ const Home = () => { id: 3, title: '다음 업데이트 예정 기능', date: '2025.06' + }, + + { + id: 4, + title: '메이플링크 비로그인 기능 업데이트 안내', + date: '2025.06.17' } ] @@ -178,60 +194,82 @@ const Home = () => {
{/* 길드 검색 */}
-
-
- - 서비스 준비중 - -
-
-
+
-
+
-

길드 검색

+

+ 길드 검색 +

+ setGuildName(e.target.value)} - className="flex-1 px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500" - disabled + onKeyPress={handleGuildKeyPress} + className="flex-1 px-4 py-2.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 bg-white" />
-

검색할 길드 목록

+

검색할 길드 목록

-
- - 아르카나 - -
-
- - 노비맙단 - -
+ {guildList.map(guild => ( +
+ {guild} + +
+ ))}
@@ -260,9 +298,14 @@ const Home = () => {
@@ -328,7 +371,7 @@ const Home = () => {
- {recentNotices.map(notice => ( + {recentNotices.reverse().map(notice => ( - {filteredItems.map(item => ( + {filteredItems.reverse().map(item => (
diff --git a/src/pages/Room.tsx b/src/pages/Room.tsx index e19004d..b313165 100644 --- a/src/pages/Room.tsx +++ b/src/pages/Room.tsx @@ -68,7 +68,7 @@ const Room = () => { mainChar: mainChar.character_name, subChar: member.name }) - openModal('alert') + openModal('alert') // 이거 나중에 삭제해야함 } catch { setAlertMessage({ mainChar: member.name, @@ -144,7 +144,7 @@ const Room = () => { setSearchCharacter={handleSearchCharacter} onDeleteGuild={ selectMember?.guildId - ? () => deleteGuild(selectMember.guildId) + ? () => deleteGuild(selectMember.guildId as number) : undefined } /> diff --git a/src/pages/SearchCharacter.tsx b/src/pages/SearchCharacter.tsx index a578277..9ca9202 100644 --- a/src/pages/SearchCharacter.tsx +++ b/src/pages/SearchCharacter.tsx @@ -1,39 +1,5 @@ -import { searchCharacterOcid } from '../apis/character/characterController' import { CharacterPage } from '../components/character/CharacterPage' -import { useState } from 'react' -import { useUserStore } from '../store/userStore' export const SearchCharacter = () => { - const [characterName, setCharacterName] = useState('') - const { setUserInfo } = useUserStore() - - const searchCharacterHandler = async () => { - if (characterName.trim() === '') { - alert('캐릭터 이름을 입력해주세요.') - return - } - - const { ocid } = await searchCharacterOcid(characterName.trim()) - - if (!ocid) { - alert('캐릭터를 찾을 수 없습니다.') - return - } - - setUserInfo({ - id: 0, - firebaseId: '1', - name: characterName.trim(), - email: 'play3step@gmail.com', - ocid: ocid - }) - } - return ( - - ) + return } diff --git a/src/pages/SearchGuild.tsx b/src/pages/SearchGuild.tsx new file mode 100644 index 0000000..c470255 --- /dev/null +++ b/src/pages/SearchGuild.tsx @@ -0,0 +1,234 @@ +import { FiUsers } from 'react-icons/fi' +import Button from '../components/common/Button' +import { useSearchGuild } from '../hooks/search/useSearchGuild' + +import { servers } from '../data/worlds' +import { MemberContainer } from '../components/guild/MemberContainer' +import { Empty } from '../components/common/Empty' +import { ActionBtnList } from '../components/guild/ActionBtnList' +import { Guild, Member } from '../types/guild' +import { useState } from 'react' +import { useModalStore } from '../store/modalStore' +import { DetailMemberModal } from '../components/modal/guild/DetailMemberModal' + +export const SearchGuild = () => { + const { + guildsInfo, + selectedServer, + setSelectedServer, + guildName, + setGuildName, + handleGuildKeyPress, + addGuildList, + removeGuildList, + guildList, + searchGuildHandler, + isLoading, + selectedGuildMember, + mainCharacterInfoSearchHandler, + isUpdating, + resetSearchParams + } = useSearchGuild() + + const [searchCharacter, setSearchCharacter] = useState('') + const { activeModal, openModal } = useModalStore() + + const [selectedMember, setSelectedMember] = useState<{ + type: string + member: Member | null + }>({ + type: '', + member: null + }) + const handleSearchCharacter = (value: string) => { + setSearchCharacter(value) + } + + const handleMemberSelect = async (type: string, member: Member) => { + setSelectedMember({ type: type, member: member }) + if (type !== '미지정') { + openModal('detailMember') + } + } + + return ( +
+ {guildsInfo?.length === 0 ? ( +
+
+
+
+ +
+

+ 길드 검색 +

+
+
+
+ + setGuildName(e.target.value)} + onKeyPress={handleGuildKeyPress} + className="flex-1 px-4 py-2.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 bg-white" + /> + +
+ +
+

검색할 길드 목록

+
+ {guildList.map(guild => ( +
+ {guild} + +
+ ))} +
+
+ + +
+
+
+ ) : ( +
+
+
+

길드 관리

+

길드원 정보 관리

+
+ +
+ +
+
+
+ ({ + worldName: v.worldName, + guildName: v.guildName + })) as Guild[]) || [] + } + mainCharacterInfoSearchHandler={ + mainCharacterInfoSearchHandler + } + isUpdating={isUpdating} + /> +
+
+
+
+ {(isLoading || isUpdating) && ( +
+
+

+ 캐릭터 정보를 불러오는 중... +

+
+ )} + {guildsInfo && + guildsInfo.length > 0 && + !isLoading && + !isUpdating && ( + ({ + guildName: v.guildName, + guildMasterName: v.guildMasterName, + memberDetailResponse: v.guildMember + }))} + masterName={selectedGuildMember?.guildMasterName} + guildName={selectedGuildMember?.guildName} + onSelect={handleMemberSelect} + searchCharacter={searchCharacter} + setSearchCharacter={handleSearchCharacter} + /> + )} + {guildsInfo && guildsInfo.length === 0 && ( + + )} +
+
+
+
+ )} + {activeModal === 'detailMember' && + selectedMember && + selectedMember.member && + guildsInfo && ( + ({ + guildName: v.guildName, + guildMasterName: v.guildMasterName, + memberDetailResponse: v.guildMember + }))} + /> + )} +
+ ) +} diff --git a/src/pages/Signup.tsx b/src/pages/Signup.tsx index 20ab52c..00900ff 100644 --- a/src/pages/Signup.tsx +++ b/src/pages/Signup.tsx @@ -15,7 +15,7 @@ const Signup = () => { const { userLogout } = useAuth() const { uid } = useAuthStore() - const { updateUserInfo } = useUserStore() + const { updateUserInfo, setCharacterOcid } = useUserStore() const onSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -41,6 +41,7 @@ const Signup = () => { nexonApiKey: result.generatedApiKey, ocid: result.characterUid }) + setCharacterOcid(result.characterUid) alert('API 키 등록이 완료되었습니다.') diff --git a/src/store/authStore.ts b/src/store/authStore.ts index bd1c450..149e9c0 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -15,11 +15,11 @@ export const useAuthStore = create(set => ({ token: null, uid: null, isLoggedIn: false, - userType: 'guest', + userType: 'search', storeLogin: (token: string, uid: string, userType: UserType) => { set({ token, uid, isLoggedIn: true, userType }) }, storeLogout: () => { - set({ token: null, uid: null, isLoggedIn: false, userType: 'guest' }) + set({ token: null, uid: null, isLoggedIn: false, userType: 'search' }) } })) diff --git a/src/store/userStore.ts b/src/store/userStore.ts index 4d490c7..cb0f681 100644 --- a/src/store/userStore.ts +++ b/src/store/userStore.ts @@ -1,27 +1,36 @@ import { create } from 'zustand' import { User } from '../types/auth' +import { guest } from '../data/guest' interface UserState { userInfo: User | null userName: string | null + characterOcid: string setUserInfo: (info: User) => void setUserName: (name: string) => void - + setCharacterOcid: (ocid: string) => void updateUserInfo: (info: Partial) => void clearUserInfo: () => void } export const useUserStore = create(set => ({ - userInfo: null, + userInfo: { + id: 0, + firebaseId: '1', + name: 'search', + email: 'play3step@gmail.com', + ocid: guest.ocid + }, userName: null, - + characterOcid: guest.ocid, setUserInfo: info => set({ userInfo: info }), setUserName: name => set({ userName: name }), - + setCharacterOcid: ocid => set({ characterOcid: ocid }), updateUserInfo: info => set(state => ({ userInfo: state.userInfo ? { ...state.userInfo, ...info } : null })), - clearUserInfo: () => set({ userInfo: null, userName: null }) + clearUserInfo: () => + set({ userInfo: null, userName: null, characterOcid: guest.ocid }) })) diff --git a/src/types/guild.ts b/src/types/guild.ts index ca71e13..fc11312 100644 --- a/src/types/guild.ts +++ b/src/types/guild.ts @@ -33,7 +33,7 @@ export interface SearchGuild { } export interface NexonMembers { - guildId: number + guildId?: number guildName: string guildMasterName?: string memberDetailResponse?: Member[] @@ -46,7 +46,7 @@ export interface RecordedMembers { } export interface Member { - id: number + id?: number imagePath: string job: string level: string @@ -59,7 +59,6 @@ export interface Member { level: string job: string imagePath: string - description: string } } @@ -74,3 +73,22 @@ export interface DetectResult { toAdd: string[] toRemove: string[] } + +export interface SearchGuildResponse { + worldName: string + guildName: string + guildMasterName: string + guildMember: Member[] +} + +export interface SearchGuildMemberResponse { + memberName: string + type: string + mainCharacterInfo: { + name: string + level: string + job: string + imagePath: string + gender: string + } +}