Skip to content
Open
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
81 changes: 81 additions & 0 deletions src/admin/UserCard.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +4 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

선언된 인터페이스 AdminData과 내용이 거의 유사한데,
이를 활용할 수는 없을까요?


// 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 (
<CardContainer isOpen={isOpen}>
<UserInfo>
<Avatar
src={icon || 'https://via.placeholder.com/50'}
alt='User Icon'
/>
<TextContainer>
<Name>{nickname}</Name>
</TextContainer>
</UserInfo>

<IconWrapper>
{isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
</IconWrapper>
</CardContainer>
);
};

export default UserCard;
105 changes: 105 additions & 0 deletions src/admin/UserList.tsx
Original file line number Diff line number Diff line change
@@ -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<AdminData[]>([]);
const [openUserId, setOpenUserId] = useState<number | null>(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 (
<UserListContainer>
{users.map((user) => (
<ListItem key={user.id}>
<div onClick={() => toggleDropdown(user.id)}>
<UserCard
nickname={user.nickname}
email={user.email}
createdAt={user.created_at}
icon={user.icon}
isOpen={openUserId === user.id}
/>
</div>

{openUserId === user.id && (
<DropdownContent>
<p>유저 ID: {user.id}</p>
<p>닉네임: {user.nickname}</p>
<p>이메일: {user.email}</p>
<p>가입 일자: {user.created_at}</p>
<DeleteButton onClick={() => handleDeleteUser(user.id)}>
회원탈퇴
</DeleteButton>
</DropdownContent>
)}
</ListItem>
))}
</UserListContainer>
);
};

export default UserList;
13 changes: 13 additions & 0 deletions src/apis/admin.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { AdminData } from '@/types/admindata';
import { httpClient } from './http.api';

export const fetchAdminData = async () => {
try {
const response = await httpClient.get<AdminData[]>('/api/admin');
console.log('응답 데이터:', response.data);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 console.log는 일반적으로는 지양되어야하는 코드입니다.
되도록이면 네트워크 탭의 정보를 통해 확인하면 더 좋을 내용인 것 같네요 👀

return response.data;
} catch (error) {
console.error('Error fetching admin data:', error);
throw error;
}
};
4 changes: 2 additions & 2 deletions src/apis/http.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 1 addition & 14 deletions src/hooks/userSlice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// userSlice.ts
import { DecodedToken, UserInfo, UserState } from '@/types/auth';
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -94,5 +81,5 @@ const userSlice = createSlice({
},
});

export const { loginSuccess, logout } = userSlice.actions;
export const { logout } = userSlice.actions;
export default userSlice.reducer;
67 changes: 63 additions & 4 deletions src/pages/AdminPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className='p-8'>
<h1 className='text-2xl font-bold'>어드민 페이지</h1>
<p>이 페이지는 관리자만 접근할 수 있습니다.</p>
</div>
<PageContainer>
{/* 제목과 아이콘 */}
<TitleContainer onClick={toggleUserList}>
<Title>유저조회</Title>
<IconWrapper>
{showUserList ? <ChevronUpIcon /> : <ChevronDownIcon />}
</IconWrapper>
</TitleContainer>

{/* 유저 리스트 */}
{showUserList && (
<ListContainer>
<UserList />
</ListContainer>
)}
</PageContainer>
);
};

Expand Down
7 changes: 7 additions & 0 deletions src/types/admindata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface AdminData {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AdminData라는 이름은 조금 헷갈릴지도 모르겠어요.
적어도 user가 네이밍 속에 들어가야 좀 더 명확해지지 않을까요? 👀

id: number;
nickname: string;
email: string;
created_at: string;
icon: string | null;
}