diff --git a/src/admin/UserCard.tsx b/src/admin/UserCard.tsx new file mode 100644 index 0000000..12c12f7 --- /dev/null +++ b/src/admin/UserCard.tsx @@ -0,0 +1,81 @@ +import styled from 'styled-components'; +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'; + +interface UserCardProps { + nickname: string; + email: string; + createdAt: string; + icon: string | null; + isOpen: boolean; +} + +// Styled Components +const CardContainer = styled.div<{ isOpen: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background-color: white; + border: 1px solid #ddd; + border-radius: ${(props) => (props.isOpen ? '8px 8px 0 0' : '8px')}; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: all 0.3s ease; + + &:hover { + background-color: #f7f7f7; + } +`; + +const UserInfo = styled.div` + display: flex; + align-items: center; +`; + +const Avatar = styled.img` + width: 50px; + height: 50px; + border-radius: 50%; + margin-right: 12px; +`; + +const TextContainer = styled.div` + display: flex; + flex-direction: column; +`; + +const Name = styled.p` + font-weight: bold; +`; + +const IconWrapper = styled.div` + width: 24px; + height: 24px; + + svg { + width: 100%; + height: 100%; + } +`; + +const UserCard = ({ nickname, icon, isOpen }: UserCardProps) => { + return ( + + + + + {nickname} + + + + + {isOpen ? : } + + + ); +}; + +export default UserCard; diff --git a/src/admin/UserList.tsx b/src/admin/UserList.tsx new file mode 100644 index 0000000..6224266 --- /dev/null +++ b/src/admin/UserList.tsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from 'react'; +import { AdminData } from '@/types/admindata'; +import UserCard from './UserCard'; +import styled from 'styled-components'; +import { fetchAdminData } from '@/apis/admin.api'; + +// Styled Components +const UserListContainer = styled.div` + width: 600px; + margin: 0; + padding-left: 10px; + max-height: 500px; + overflow-y: auto; + border: 1px solid #ddd; + border-radius: 8px; +`; + +const ListItem = styled.div` + margin-bottom: 12px; /* 리스트 아이템 간 간격 추가 */ + margin-top: 12px; + margin-right: 12px; +`; + +const DropdownContent = styled.div` + background-color: #f9f9f9; + padding: 16px; + border: 1px solid #ddd; + border-top: none; + border-radius: 0 0 8px 8px; + box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.1); +`; + +const DeleteButton = styled.button` + background-color: #ff4d4f; + color: white; + padding: 8px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + margin-top: 10px; + transition: background-color 0.3s; + + &:hover { + background-color: #e33e3e; + } +`; + +const UserList = () => { + const [users, setUsers] = useState([]); + const [openUserId, setOpenUserId] = useState(null); + + const toggleDropdown = (id: number) => { + setOpenUserId((prev) => (prev === id ? null : id)); + }; + + const handleDeleteUser = (id: number) => { + alert(`회원탈퇴: 유저 ID ${id}`); + }; + + useEffect(() => { + const loadUsers = async () => { + try { + const data = await fetchAdminData(); + setUsers(data); + } catch (error) { + console.error('Failed to load users:', error); + } + }; + + loadUsers(); + }, []); + + return ( + + {users.map((user) => ( + +
toggleDropdown(user.id)}> + +
+ + {openUserId === user.id && ( + +

유저 ID: {user.id}

+

닉네임: {user.nickname}

+

이메일: {user.email}

+

가입 일자: {user.created_at}

+ handleDeleteUser(user.id)}> + 회원탈퇴 + +
+ )} +
+ ))} +
+ ); +}; + +export default UserList; diff --git a/src/apis/admin.api.ts b/src/apis/admin.api.ts new file mode 100644 index 0000000..7c2ecc4 --- /dev/null +++ b/src/apis/admin.api.ts @@ -0,0 +1,13 @@ +import { AdminData } from '@/types/admindata'; +import { httpClient } from './http.api'; + +export const fetchAdminData = async () => { + try { + const response = await httpClient.get('/api/admin'); + console.log('응답 데이터:', response.data); + return response.data; + } catch (error) { + console.error('Error fetching admin data:', error); + throw error; + } +}; diff --git a/src/apis/http.api.ts b/src/apis/http.api.ts index 66121c4..e36cc7c 100644 --- a/src/apis/http.api.ts +++ b/src/apis/http.api.ts @@ -3,10 +3,10 @@ import axios, { AxiosRequestConfig } from 'axios'; import { getToken, removeToken } from '@/utils/token'; export const BASE_URL = import.meta.env.VITE_API_BASE_URL; -const DEFAULT_TIMEOUT = 30000; // 요청 제한 시간 +const DEFAULT_TIMEOUT = 30000; export const createClient = (config?: AxiosRequestConfig) => { - const token = getToken(); // 토큰 가져오기 + const token = getToken(); const axiosInstance = axios.create({ baseURL: BASE_URL, timeout: DEFAULT_TIMEOUT, diff --git a/src/hooks/userSlice.ts b/src/hooks/userSlice.ts index ef9bd3f..882cabb 100644 --- a/src/hooks/userSlice.ts +++ b/src/hooks/userSlice.ts @@ -1,4 +1,3 @@ -// userSlice.ts import { DecodedToken, UserInfo, UserState } from '@/types/auth'; import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from 'axios'; @@ -42,18 +41,6 @@ const userSlice = createSlice({ name: 'user', initialState, reducers: { - loginSuccess: ( - state, - action: PayloadAction<{ token: string; userInfo: UserInfo }> - ) => { - state.isLoggedIn = true; - state.token = action.payload.token; - state.userInfo = action.payload.userInfo; - state.loading = false; - state.error = null; - - localStorage.setItem('token', action.payload.token); - }, logout: (state) => { state.isLoggedIn = false; state.token = null; @@ -94,5 +81,5 @@ const userSlice = createSlice({ }, }); -export const { loginSuccess, logout } = userSlice.actions; +export const { logout } = userSlice.actions; export default userSlice.reducer; diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx index b1c6f4d..c2d1874 100644 --- a/src/pages/AdminPage.tsx +++ b/src/pages/AdminPage.tsx @@ -1,9 +1,68 @@ +import { useState } from 'react'; +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'; +import UserList from '@/admin/UserList'; +import styled from 'styled-components'; + +// Styled Components +const PageContainer = styled.div` + padding: 20px; +`; + +const TitleContainer = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; + cursor: pointer; + margin-left: 10px; + margin-right: 10px; +`; + +const Title = styled.h1` + font-family: 'Pretendard', sans-serif; + font-size: 32px; + font-weight: 800; + text-align: left; + margin: 0; +`; + +const IconWrapper = styled.div` + width: 24px; + height: 24px; + margin-left: 10px; +`; + +const ListContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + margin-top: 40px; +`; + const AdminPage = () => { + const [showUserList, setShowUserList] = useState(false); + + const toggleUserList = () => { + setShowUserList((open) => !open); + }; + return ( -
-

어드민 페이지

-

이 페이지는 관리자만 접근할 수 있습니다.

-
+ + {/* 제목과 아이콘 */} + + 유저조회 + + {showUserList ? : } + + + + {/* 유저 리스트 */} + {showUserList && ( + + + + )} + ); }; diff --git a/src/types/admindata.ts b/src/types/admindata.ts new file mode 100644 index 0000000..55f18d6 --- /dev/null +++ b/src/types/admindata.ts @@ -0,0 +1,7 @@ +export interface AdminData { + id: number; + nickname: string; + email: string; + created_at: string; + icon: string | null; +}