Skip to content

Commit 32ece59

Browse files
authored
Merge pull request #56 from play3step/refactor/guild-manager
[refactor] 그룹 관리 컴포넌트 분리 및 모바일 버전
2 parents 3ac58cf + 77b244b commit 32ece59

11 files changed

Lines changed: 398 additions & 238 deletions

File tree

src/components/guild/MemberContainer.tsx

Lines changed: 83 additions & 99 deletions
Large diffs are not rendered by default.

src/components/layout/ProtectedRoute.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Navigate, useLocation } from 'react-router-dom'
22
import { useAuthStore } from '../../store/authStore'
33
import { useUserStore } from '../../store/userStore'
44
import { useAuth } from '../../hooks/useAuth'
5+
import { useEffect } from 'react'
6+
57
interface ProtectedRouteProps {
68
children: React.ReactNode
79
}
@@ -14,9 +16,16 @@ export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
1416

1517
const path = location.pathname
1618

19+
// useEffect로 사이드 이펙트 처리
20+
useEffect(() => {
21+
// 1. 로그인도 안 됐는데 루트가 아닌 경로 접근 시
22+
if (!isLoggedIn && path !== '/') {
23+
userLogout()
24+
}
25+
}, [isLoggedIn, path, userLogout])
26+
1727
// 1. 로그인도 안 됐는데 루트가 아닌 경로 접근 시
1828
if (!isLoggedIn && path !== '/') {
19-
userLogout()
2029
return <Navigate to="/" />
2130
}
2231

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const EmptyRoomState = () => {
2+
return (
3+
<div className="h-screen flex items-center justify-center">
4+
<p className="text-gray-600">관리방이 없습니다.</p>
5+
</div>
6+
)
7+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ModalType } from '../../store/modalStore'
2+
import { ActionBtnList } from '../guild/ActionBtnList'
3+
import { ListSwitch } from '../guild/ListSwitch'
4+
import { Guild } from '../../types/guild'
5+
6+
interface Props {
7+
showModal: (name: ModalType) => void
8+
guildList: Guild[]
9+
handleDetect: () => void
10+
refreshMember: (guildId: number) => void
11+
}
12+
13+
export const RoomActionBar = ({
14+
showModal,
15+
guildList,
16+
handleDetect,
17+
refreshMember
18+
}: Props) => {
19+
return (
20+
<div className="p-3 sm:p-4 lg:p-6 border-b border-gray-100">
21+
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 sm:gap-4">
22+
<div className="order-2 sm:order-1">
23+
<ActionBtnList
24+
showModal={showModal}
25+
guildList={guildList}
26+
handleDetect={handleDetect}
27+
refreshMember={refreshMember}
28+
/>
29+
</div>
30+
{guildList.length > 0 && (
31+
<div className="order-1 sm:order-2 flex justify-center sm:justify-end">
32+
<ListSwitch />
33+
</div>
34+
)}
35+
</div>
36+
</div>
37+
)
38+
}

src/components/room/RoomCard.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { IoTrashOutline, IoPersonAddOutline } from 'react-icons/io5'
2+
import { Room } from '../../types/rooms'
3+
4+
interface Props {
5+
room: Room
6+
onEnterRoom: (room: Room) => void
7+
onManageRoom: (room: Room) => void
8+
onDeleteRoom: (adminId: string) => void
9+
}
10+
11+
export const RoomCard = ({
12+
room,
13+
onEnterRoom,
14+
onManageRoom,
15+
onDeleteRoom
16+
}: Props) => {
17+
return (
18+
<div className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100">
19+
<div className="p-6">
20+
<div className="flex justify-between items-start mb-4">
21+
<div>
22+
<h2 className="text-xl font-bold text-gray-900 mb-1">
23+
{room.groupName}
24+
</h2>
25+
</div>
26+
</div>
27+
28+
<div className="flex items-center gap-4 mb-6">
29+
<div className="flex-1 bg-gray-50 rounded-lg p-4">
30+
<p className="text-sm text-gray-600">메인 길드</p>
31+
<p className="text-lg font-bold text-gray-900">
32+
{room.mainGuild.name}
33+
</p>
34+
</div>
35+
</div>
36+
37+
<div className="flex gap-3">
38+
<button
39+
onClick={() => onEnterRoom(room)}
40+
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">
41+
관리방 입장
42+
</button>
43+
<button
44+
className="flex items-center justify-center w-10 h-10 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-lg transition-colors"
45+
title="관리자 추가"
46+
onClick={() => onManageRoom(room)}>
47+
<IoPersonAddOutline className="text-xl" />
48+
</button>
49+
<button
50+
className="flex items-center justify-center w-10 h-10 bg-red-50 hover:bg-red-100 text-red-500 rounded-lg transition-colors"
51+
title="삭제"
52+
onClick={() => onDeleteRoom(room.adminId.toString())}>
53+
<IoTrashOutline className="text-xl" />
54+
</button>
55+
</div>
56+
</div>
57+
</div>
58+
)
59+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Guild, Member, NexonMembers } from '../../types/guild'
2+
import { MemberContainer } from '../guild/MemberContainer'
3+
import { Empty } from '../common/Empty'
4+
5+
interface Props {
6+
nexonMembersLoading: boolean
7+
guildList: Guild[]
8+
selectMember: NexonMembers
9+
nexonMembers: NexonMembers[] | null
10+
main: string | undefined
11+
searchCharacter: string
12+
onMemberSelect: (type: string, member: Member) => void
13+
onSearchCharacter: (value: string) => void
14+
onDeleteGuild?: () => void
15+
}
16+
17+
export const RoomContent = ({
18+
nexonMembersLoading,
19+
guildList,
20+
selectMember,
21+
nexonMembers,
22+
main,
23+
searchCharacter,
24+
onMemberSelect,
25+
onSearchCharacter,
26+
onDeleteGuild
27+
}: Props) => {
28+
return (
29+
<div className="p-3 sm:p-4 lg:p-6">
30+
<div className="min-h-[400px] sm:min-h-[500px] lg:min-h-[600px]">
31+
{nexonMembersLoading && (
32+
<div className="flex flex-col justify-center items-center h-full py-12">
33+
<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>
34+
<p className="text-gray-600 font-medium text-sm sm:text-base text-center px-4">
35+
캐릭터 정보를 불러오는 중...
36+
</p>
37+
</div>
38+
)}
39+
{guildList.length > 0 ? (
40+
<MemberContainer
41+
members={selectMember?.memberDetailResponse as Member[]}
42+
allMembers={nexonMembers as NexonMembers[]}
43+
masterName={selectMember?.guildMasterName}
44+
guildName={selectMember?.guildName}
45+
onSelect={onMemberSelect}
46+
isMainGuild={selectMember?.guildName === main}
47+
searchCharacter={searchCharacter}
48+
setSearchCharacter={onSearchCharacter}
49+
onDeleteGuild={onDeleteGuild}
50+
/>
51+
) : (
52+
<div className="flex justify-center items-center h-full">
53+
<Empty text="길드를 선택해주세요" />
54+
</div>
55+
)}
56+
</div>
57+
</div>
58+
)
59+
}

src/components/room/RoomGrid.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Room } from '../../types/rooms'
2+
import { RoomCard } from './RoomCard'
3+
4+
interface Props {
5+
rooms: Room[]
6+
onEnterRoom: (room: Room) => void
7+
onManageRoom: (room: Room) => void
8+
onDeleteRoom: (adminId: string) => void
9+
}
10+
11+
export const RoomGrid = ({
12+
rooms,
13+
onEnterRoom,
14+
onManageRoom,
15+
onDeleteRoom
16+
}: Props) => {
17+
return (
18+
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
19+
{rooms.map(room => (
20+
<RoomCard
21+
key={room.adminId}
22+
room={room}
23+
onEnterRoom={onEnterRoom}
24+
onManageRoom={onManageRoom}
25+
onDeleteRoom={onDeleteRoom}
26+
/>
27+
))}
28+
</div>
29+
)
30+
}

src/components/room/RoomHeader.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { IoArrowBack } from 'react-icons/io5'
2+
3+
interface Props {
4+
onBack: () => void
5+
}
6+
7+
export const RoomHeader = ({ onBack }: Props) => {
8+
return (
9+
<div className="flex items-center gap-3 sm:gap-4 mb-4 sm:mb-5 px-1 sm:px-0">
10+
<button
11+
onClick={onBack}
12+
className="p-2 hover:bg-gray-100 rounded-lg transition-colors flex-shrink-0"
13+
title="뒤로 가기">
14+
<IoArrowBack className="text-lg sm:text-xl text-gray-600" />
15+
</button>
16+
<div className="min-w-0 flex-1">
17+
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 truncate">
18+
길드 관리
19+
</h1>
20+
<p className="text-xs sm:text-sm text-gray-600 mt-0.5 sm:mt-1 truncate">
21+
길드원 정보 관리
22+
</p>
23+
</div>
24+
</div>
25+
)
26+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { IoAdd } from 'react-icons/io5'
2+
3+
interface Props {
4+
onCreateRoom: () => void
5+
}
6+
7+
export const RoomListHeader = ({ onCreateRoom }: Props) => {
8+
return (
9+
<div className="flex justify-between items-center mb-8">
10+
<div>
11+
<h1 className="text-3xl font-bold text-gray-900">길드 관리 홈</h1>
12+
<p className="text-gray-600 mt-2">관리중인 길드방 목록입니다</p>
13+
</div>
14+
<div className="relative group">
15+
<button
16+
onClick={onCreateRoom}
17+
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">
18+
<IoAdd className="text-xl" />새 관리방
19+
</button>
20+
<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">
21+
<div className="bg-gray-800 text-white text-base px-3 py-2 rounded-lg shadow-lg whitespace-nowrap">
22+
⚠️ 관리방 생성은 길드 마스터만 가능합니다.
23+
<br /> 마스터가 아니라면, 해당 마스터가 생성후 그룹에 초대해 주세요.
24+
</div>
25+
</div>
26+
</div>
27+
</div>
28+
)
29+
}

src/pages/Room.tsx

Lines changed: 25 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
import { CreateGuildModal } from '../components/modal/guild/CreateGuildModal'
22
import { ModalType, useModalStore } from '../store/modalStore'
33
import { useGuildsList } from '../hooks/guild/useGuildsList'
4-
import { MemberContainer } from '../components/guild/MemberContainer'
5-
import { ListSwitch } from '../components/guild/ListSwitch'
6-
import { ActionBtnList } from '../components/guild/ActionBtnList'
74
import { DetectMemberModal } from '../components/modal/guild/DetectMemberModal'
85
import { useGuildMember } from '../hooks/guild/useGuildMember'
9-
import { Empty } from '../components/common/Empty'
106
import { DetailMemberModal } from '../components/modal/guild/DetailMemberModal'
117
import { Loading } from '../components/common/Loading'
128
import { useState } from 'react'
139
import { Member, NexonMembers } from '../types/guild'
14-
import { IoArrowBack } from 'react-icons/io5'
1510
import { useNavigate } from 'react-router-dom'
1611
import { useGuildDetect } from '../hooks/guild/useGuildDetect'
1712
import { findMainCharacter } from '../apis/character/characterController'
1813
import { AlertModal } from '../components/modal/common/AlertModal'
14+
import { RoomHeader } from '../components/room/RoomHeader'
15+
import { RoomActionBar } from '../components/room/RoomActionBar'
16+
import { RoomContent } from '../components/room/RoomContent'
1917

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

11299
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
113-
<div className="p-6 border-b border-gray-100">
114-
<div className="flex justify-between items-center">
115-
<ActionBtnList
116-
showModal={showModal}
117-
guildList={guildList}
118-
handleDetect={handleDetect}
119-
refreshMember={refreshMember}
120-
/>
121-
{guildList.length > 0 && <ListSwitch />}
122-
</div>
123-
</div>
100+
<RoomActionBar
101+
showModal={showModal}
102+
guildList={guildList}
103+
handleDetect={handleDetect}
104+
refreshMember={refreshMember as (guildId: number) => void}
105+
/>
124106

125-
<div className="p-6">
126-
<div className="min-h-[600px]">
127-
{nexonMembersLoading && (
128-
<div className="flex justify-center items-center h-full">
129-
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
130-
<p className="text-gray-600 font-medium">
131-
캐릭터 정보를 불러오는 중...
132-
</p>
133-
</div>
134-
)}
135-
{guildList.length > 0 ? (
136-
<MemberContainer
137-
members={selectMember?.memberDetailResponse as Member[]}
138-
allMembers={nexonMembers as NexonMembers[]}
139-
masterName={selectMember?.guildMasterName}
140-
guildName={selectMember?.guildName}
141-
onSelect={handleMemberSelect}
142-
isMainGuild={selectMember?.guildName === main}
143-
searchCharacter={searchCharacter}
144-
setSearchCharacter={handleSearchCharacter}
145-
onDeleteGuild={
146-
selectMember?.guildId
147-
? () => deleteGuild(selectMember.guildId as number)
148-
: undefined
149-
}
150-
/>
151-
) : (
152-
<Empty text="길드를 선택해주세요" />
153-
)}
154-
</div>
155-
</div>
107+
<RoomContent
108+
nexonMembersLoading={nexonMembersLoading as boolean}
109+
guildList={guildList}
110+
selectMember={selectMember as NexonMembers}
111+
nexonMembers={nexonMembers as NexonMembers[]}
112+
main={main}
113+
searchCharacter={searchCharacter}
114+
onMemberSelect={handleMemberSelect}
115+
onSearchCharacter={handleSearchCharacter}
116+
onDeleteGuild={
117+
selectMember?.guildId
118+
? () => deleteGuild(selectMember.guildId as number)
119+
: undefined
120+
}
121+
/>
156122
</div>
157123
</div>
158124

0 commit comments

Comments
 (0)