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
54 changes: 54 additions & 0 deletions src/components/UserList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ChevronDown } from "lucide-react";
import UserListCard from "./UserListCard";
import { useEffect, useState } from "react";
import { useUserStore } from "../stores/userListStore";
import { useLocation } from "react-router";

export default function UserList() {
const [isOpen, setIsOpen] = useState(false);
const location = useLocation();
const { userList, fetchUsers } = useUserStore();

// 페이지 이동 시 자동으로 닫기
useEffect(() => {
setIsOpen(false);
}, [location.pathname]);

useEffect(() => {
fetchUsers();
}, [fetchUsers]);

/* 필요한 정보 */
/* 프로필 이미지, 이름, 소속, 학년(전공 과목), auth_id */
return (
<div className="absolute left-10 bottom-0 w-90 bg-white rounded-t-xl flex flex-col">
{/* 헤더 */}
<div
className="cursor-pointer flex flex-row justify-between items-center px-5 py-3 w-full bg-violet-500 rounded-t-xl"
onClick={() => setIsOpen(!isOpen)}
>
<h3 className="text-lg text-white font-bold">온라인 사용자</h3>
<ChevronDown
className={`text-white transition-transform duration-300 ${
isOpen ? "rotate-0" : "rotate-180"
}`}
size={28}
strokeWidth={3}
/>
</div>

{/* 슬라이딩 리스트 */}
<div
className={` transition-all duration-500 ease-in-out pb-4 ${
isOpen ? "h-160 overflow-y-auto" : "h-0 overflow-y-hidden"
}`}
>
<div className="flex flex-col">
{userList.map((user) => (
<UserListCard key={user.auth_id} user={user} />
))}
</div>
</div>
</div>
);
}
53 changes: 53 additions & 0 deletions src/components/UserListCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Link } from "react-router";
import { getAge } from "../utils/getAge";
import { getGrade } from "../utils/getGrade";

export default function UserListCard({ user }: { user: User }) {
const age = user.birth_date ? getAge(user.birth_date) : 0;
const grade = user.role === "student" && getGrade(age);

const roleMap: Record<string, string> = {
student: "학생",
teacher: "선생님",
parent: "학부모",
};

return (
<>
<Link to={`/profile/${user.auth_id}`}>
<div className="flex flex-row justify-between items-center py-4 px-5 hover:bg-[#F1F3F5]">
{/* left */}
<div className="flex flex-row items-center gap-2.5">
{/* 이미지 */}
<div className="relative w-12 h-12 bg-amber-300 rounded-lg">
{/* 온라인 */}
<div className="absolute right-0 bottom-0 w-4 h-4 bg-green-500 rounded-full border-2 border-white"></div>
{/* 오프라인 */}
{/* <div className="absolute right-0 bottom-0 w-4 h-4 bg-gray-400 rounded-full border-2 border-white"></div> */}
</div>
{/* 정보 */}
<div className="flex flex-col gap-1 text-sm">
{/* 이름 */}
<div className="line-clamp-1">{user.nickname}</div>
{/* 소속, 학년 (선생님은 전공) */}
<div>
{roleMap[user.role] || "알 수 없음"}
{user.role === "student" && `, ${grade}`}{" "}
{user.role === "teacher" && `, ${user.major}`}
</div>
</div>
</div>
{/* right */}
<div className="space-x-2 text-xs">
<button className="cursor-pointer px-2 py-1 bg-violet-500 text-white rounded hover:bg-violet-600">
팔로우
</button>
<button className="cursor-pointer px-2 py-1 bg-gray-200 rounded hover:bg-gray-300">
메시지
</button>
</div>
</div>
</Link>
</>
);
}
5 changes: 4 additions & 1 deletion src/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Outlet } from "react-router";
import Header from "../components/layout/Header";
import Footer from "../components/layout/Footer";
import UserList from "../components/UserList";

export default function MainLayout() {
return (
Expand All @@ -11,13 +12,15 @@ export default function MainLayout() {
<Header />

{/* 스크롤 영역 */}
<main className="overflow-y-auto bg-[#F3F4F6] flex flex-col items-center h-[calc(100vh-70px)]">
<main className="relative overflow-y-auto bg-[#F3F4F6] flex flex-col items-center h-[calc(100vh-70px)]">
<div className="min-h-[calc(100%)]">
<div className="min-h-[calc(100%-90px)] pt-10">
<Outlet />
</div>
<Footer />
</div>
{/* 유저 리스트 모달 */}
<UserList />
</main>
</div>
</>
Expand Down
28 changes: 28 additions & 0 deletions src/stores/userListStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import supabase from "../utils/supabase";

type UserListState = {
userList: User[];
// fetchUsers: 전체 유저 리스트 가져오기
fetchUsers: () => Promise<void>;
};

export const useUserStore = create<UserListState>()(
immer((set) => ({
userList: [],
// 전체 유저 리스트 가져오기
fetchUsers: async () => {
try {
const { data, error } = await supabase
.from("users")
.select("*");
if (error) throw error;

set({ userList: data || [] });
} catch (err) {
console.error("유저 목록 로딩 오류:", err);
}
},
})),
);
8 changes: 8 additions & 0 deletions src/types/user.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type User = {
auth_id: string;
nickname: string;
online?: string;
role: string;
birth_date?: Date;
major?: string;
};