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
182 changes: 83 additions & 99 deletions src/components/guild/MemberContainer.tsx

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion src/components/layout/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Navigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '../../store/authStore'
import { useUserStore } from '../../store/userStore'
import { useAuth } from '../../hooks/useAuth'
import { useEffect } from 'react'

interface ProtectedRouteProps {
children: React.ReactNode
}
Expand All @@ -14,9 +16,16 @@ export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {

const path = location.pathname

// useEffect로 사이드 이펙트 처리
useEffect(() => {
// 1. 로그인도 안 됐는데 루트가 아닌 경로 접근 시
if (!isLoggedIn && path !== '/') {
userLogout()
}
}, [isLoggedIn, path, userLogout])

// 1. 로그인도 안 됐는데 루트가 아닌 경로 접근 시
if (!isLoggedIn && path !== '/') {
userLogout()
return <Navigate to="/" />
}

Expand Down
7 changes: 7 additions & 0 deletions src/components/room/EmptyRoomState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const EmptyRoomState = () => {
return (
<div className="h-screen flex items-center justify-center">
<p className="text-gray-600">관리방이 없습니다.</p>
</div>
)
}
38 changes: 38 additions & 0 deletions src/components/room/RoomActionBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ModalType } from '../../store/modalStore'
import { ActionBtnList } from '../guild/ActionBtnList'
import { ListSwitch } from '../guild/ListSwitch'
import { Guild } from '../../types/guild'

interface Props {
showModal: (name: ModalType) => void
guildList: Guild[]
handleDetect: () => void
refreshMember: (guildId: number) => void
}

export const RoomActionBar = ({
showModal,
guildList,
handleDetect,
refreshMember
}: Props) => {
return (
<div className="p-3 sm:p-4 lg:p-6 border-b border-gray-100">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 sm:gap-4">
<div className="order-2 sm:order-1">
<ActionBtnList
showModal={showModal}
guildList={guildList}
handleDetect={handleDetect}
refreshMember={refreshMember}
/>
</div>
{guildList.length > 0 && (
<div className="order-1 sm:order-2 flex justify-center sm:justify-end">
<ListSwitch />
</div>
)}
</div>
</div>
)
}
59 changes: 59 additions & 0 deletions src/components/room/RoomCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { IoTrashOutline, IoPersonAddOutline } from 'react-icons/io5'
import { Room } from '../../types/rooms'

interface Props {
room: Room
onEnterRoom: (room: Room) => void
onManageRoom: (room: Room) => void
onDeleteRoom: (adminId: string) => void
}

export const RoomCard = ({
room,
onEnterRoom,
onManageRoom,
onDeleteRoom
}: Props) => {
return (
<div className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100">
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<div>
<h2 className="text-xl font-bold text-gray-900 mb-1">
{room.groupName}
</h2>
</div>
</div>

<div className="flex items-center gap-4 mb-6">
<div className="flex-1 bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-600">메인 길드</p>
<p className="text-lg font-bold text-gray-900">
{room.mainGuild.name}
</p>
</div>
</div>

<div className="flex gap-3">
<button
onClick={() => onEnterRoom(room)}
className="flex-1 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2.5 rounded-lg font-medium transition-colors text-sm">
관리방 입장
</button>
<button
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-lg transition-colors"
title="관리자 추가"
onClick={() => onManageRoom(room)}>
<IoPersonAddOutline className="text-xl" />
</button>
<button
className="flex items-center justify-center w-10 h-10 bg-red-50 hover:bg-red-100 text-red-500 rounded-lg transition-colors"
title="삭제"
onClick={() => onDeleteRoom(room.adminId.toString())}>
<IoTrashOutline className="text-xl" />
</button>
</div>
</div>
</div>
)
}
59 changes: 59 additions & 0 deletions src/components/room/RoomContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Guild, Member, NexonMembers } from '../../types/guild'
import { MemberContainer } from '../guild/MemberContainer'
import { Empty } from '../common/Empty'

interface Props {
nexonMembersLoading: boolean
guildList: Guild[]
selectMember: NexonMembers
nexonMembers: NexonMembers[] | null
main: string | undefined
searchCharacter: string
onMemberSelect: (type: string, member: Member) => void
onSearchCharacter: (value: string) => void
onDeleteGuild?: () => void
}

export const RoomContent = ({
nexonMembersLoading,
guildList,
selectMember,
nexonMembers,
main,
searchCharacter,
onMemberSelect,
onSearchCharacter,
onDeleteGuild
}: Props) => {
return (
<div className="p-3 sm:p-4 lg:p-6">
<div className="min-h-[400px] sm:min-h-[500px] lg:min-h-[600px]">
{nexonMembersLoading && (
<div className="flex flex-col justify-center items-center h-full py-12">
<div className="animate-spin rounded-full h-10 w-10 sm:h-12 sm:w-12 border-t-2 border-b-2 border-blue-500 mb-3 sm:mb-4"></div>
<p className="text-gray-600 font-medium text-sm sm:text-base text-center px-4">
캐릭터 정보를 불러오는 중...
</p>
</div>
)}
{guildList.length > 0 ? (
<MemberContainer
members={selectMember?.memberDetailResponse as Member[]}
allMembers={nexonMembers as NexonMembers[]}
masterName={selectMember?.guildMasterName}
guildName={selectMember?.guildName}
onSelect={onMemberSelect}
isMainGuild={selectMember?.guildName === main}
searchCharacter={searchCharacter}
setSearchCharacter={onSearchCharacter}
onDeleteGuild={onDeleteGuild}
/>
) : (
<div className="flex justify-center items-center h-full">
<Empty text="길드를 선택해주세요" />
</div>
)}
</div>
</div>
)
}
30 changes: 30 additions & 0 deletions src/components/room/RoomGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Room } from '../../types/rooms'
import { RoomCard } from './RoomCard'

interface Props {
rooms: Room[]
onEnterRoom: (room: Room) => void
onManageRoom: (room: Room) => void
onDeleteRoom: (adminId: string) => void
}

export const RoomGrid = ({
rooms,
onEnterRoom,
onManageRoom,
onDeleteRoom
}: Props) => {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{rooms.map(room => (
<RoomCard
key={room.adminId}
room={room}
onEnterRoom={onEnterRoom}
onManageRoom={onManageRoom}
onDeleteRoom={onDeleteRoom}
/>
))}
</div>
)
}
26 changes: 26 additions & 0 deletions src/components/room/RoomHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { IoArrowBack } from 'react-icons/io5'

interface Props {
onBack: () => void
}

export const RoomHeader = ({ onBack }: Props) => {
return (
<div className="flex items-center gap-3 sm:gap-4 mb-4 sm:mb-5 px-1 sm:px-0">
<button
onClick={onBack}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors flex-shrink-0"
title="뒤로 가기">
<IoArrowBack className="text-lg sm:text-xl text-gray-600" />
</button>
<div className="min-w-0 flex-1">
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 truncate">
길드 관리
</h1>
<p className="text-xs sm:text-sm text-gray-600 mt-0.5 sm:mt-1 truncate">
길드원 정보 관리
</p>
</div>
</div>
)
}
29 changes: 29 additions & 0 deletions src/components/room/RoomListHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { IoAdd } from 'react-icons/io5'

interface Props {
onCreateRoom: () => void
}

export const RoomListHeader = ({ onCreateRoom }: Props) => {
return (
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">길드 관리 홈</h1>
<p className="text-gray-600 mt-2">관리중인 길드방 목록입니다</p>
</div>
<div className="relative group">
<button
onClick={onCreateRoom}
className="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold transition-all duration-200 shadow-md hover:shadow-lg">
<IoAdd className="text-xl" />새 관리방
</button>
<div className="absolute right-0 -bottom-1 translate-y-full invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all duration-200 z-20">
<div className="bg-gray-800 text-white text-base px-3 py-2 rounded-lg shadow-lg whitespace-nowrap">
⚠️ 관리방 생성은 길드 마스터만 가능합니다.
<br /> 마스터가 아니라면, 해당 마스터가 생성후 그룹에 초대해 주세요.
</div>
</div>
</div>
</div>
)
}
84 changes: 25 additions & 59 deletions src/pages/Room.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { CreateGuildModal } from '../components/modal/guild/CreateGuildModal'
import { ModalType, useModalStore } from '../store/modalStore'
import { useGuildsList } from '../hooks/guild/useGuildsList'
import { MemberContainer } from '../components/guild/MemberContainer'
import { ListSwitch } from '../components/guild/ListSwitch'
import { ActionBtnList } from '../components/guild/ActionBtnList'
import { DetectMemberModal } from '../components/modal/guild/DetectMemberModal'
import { useGuildMember } from '../hooks/guild/useGuildMember'
import { Empty } from '../components/common/Empty'
import { DetailMemberModal } from '../components/modal/guild/DetailMemberModal'
import { Loading } from '../components/common/Loading'
import { useState } from 'react'
import { Member, NexonMembers } from '../types/guild'
import { IoArrowBack } from 'react-icons/io5'
import { useNavigate } from 'react-router-dom'
import { useGuildDetect } from '../hooks/guild/useGuildDetect'
import { findMainCharacter } from '../apis/character/characterController'
import { AlertModal } from '../components/modal/common/AlertModal'
import { RoomHeader } from '../components/room/RoomHeader'
import { RoomActionBar } from '../components/room/RoomActionBar'
import { RoomContent } from '../components/room/RoomContent'

const Room = () => {
const { activeModal, openModal } = useModalStore()
Expand Down Expand Up @@ -96,63 +94,31 @@ const Room = () => {
return (
<div className="w-full min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 py-5">
<div className="flex items-center gap-4 mb-5">
<button
onClick={() => navigate('/rooms')}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
title="뒤로 가기">
<IoArrowBack className="text-xl text-gray-600" />
</button>
<div>
<h1 className="text-2xl font-bold text-gray-900">길드 관리</h1>
<p className="text-sm text-gray-600 mt-1">길드원 정보 관리</p>
</div>
</div>
<RoomHeader onBack={() => navigate('/rooms')} />

<div className="bg-white rounded-xl shadow-sm border border-gray-100">
<div className="p-6 border-b border-gray-100">
<div className="flex justify-between items-center">
<ActionBtnList
showModal={showModal}
guildList={guildList}
handleDetect={handleDetect}
refreshMember={refreshMember}
/>
{guildList.length > 0 && <ListSwitch />}
</div>
</div>
<RoomActionBar
showModal={showModal}
guildList={guildList}
handleDetect={handleDetect}
refreshMember={refreshMember as (guildId: number) => void}
/>

<div className="p-6">
<div className="min-h-[600px]">
{nexonMembersLoading && (
<div className="flex justify-center items-center h-full">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
<p className="text-gray-600 font-medium">
캐릭터 정보를 불러오는 중...
</p>
</div>
)}
{guildList.length > 0 ? (
<MemberContainer
members={selectMember?.memberDetailResponse as Member[]}
allMembers={nexonMembers as NexonMembers[]}
masterName={selectMember?.guildMasterName}
guildName={selectMember?.guildName}
onSelect={handleMemberSelect}
isMainGuild={selectMember?.guildName === main}
searchCharacter={searchCharacter}
setSearchCharacter={handleSearchCharacter}
onDeleteGuild={
selectMember?.guildId
? () => deleteGuild(selectMember.guildId as number)
: undefined
}
/>
) : (
<Empty text="길드를 선택해주세요" />
)}
</div>
</div>
<RoomContent
nexonMembersLoading={nexonMembersLoading as boolean}
guildList={guildList}
selectMember={selectMember as NexonMembers}
nexonMembers={nexonMembers as NexonMembers[]}
main={main}
searchCharacter={searchCharacter}
onMemberSelect={handleMemberSelect}
onSearchCharacter={handleSearchCharacter}
onDeleteGuild={
selectMember?.guildId
? () => deleteGuild(selectMember.guildId as number)
: undefined
}
/>
</div>
</div>

Expand Down
Loading