-
Notifications
You must be signed in to change notification settings - Fork 2
[feat] 관리자 회원 상세 페이지 UI 구현 #145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| "use client"; | ||
|
|
||
| import React, { useState } from "react"; | ||
| import AdminUserProfile from "@/components/base-ui/Admin/users/AdminUserProfile"; | ||
| import AdminCategory from "@/components/base-ui/Admin/users/AdminCategory"; | ||
| import AdminUserTabs, { | ||
| type AdminUserTabId, | ||
| } from "@/components/base-ui/Admin/users/AdminUserTab"; | ||
|
|
||
| import MeetingList from "@/components/base-ui/Admin/users/MeetingList"; | ||
| import BookStoryList from "@/components/base-ui/Admin/users/BookStoryList"; | ||
| import NewsList from "@/components/base-ui/Admin/users/NewsList"; | ||
| import ReportList from "@/components/base-ui/Admin/users/ReportList"; | ||
|
|
||
|
|
||
| export default function Page() { | ||
| const [activeTab, setActiveTab] = | ||
| useState<AdminUserTabId>("meetings"); | ||
|
|
||
| // 더미 | ||
| const user = { | ||
| userId: "hy_0716", | ||
| name: "윤현일", | ||
| email: "yh9839@naver.com", | ||
| phone: "010-1234-5678", | ||
| intro: | ||
| "이건 다양한 책을 읽고 서로의 생각을 나누는 책무새라서 시작했습니다. 한 권의 책이 주는 작은 울림이 일상에 변화를 만든다고 믿어요.", | ||
| profileImageUrl: null as string | null, | ||
| }; | ||
|
|
||
| const selectedCategories = [ | ||
| "KOREAN_NOVEL", | ||
| "ESSAY", | ||
| "HUMANITIES", | ||
| "SELF_IMPROVEMENT", | ||
| ]; | ||
|
Comment on lines
+31
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| return ( | ||
| <main className="w-full bg-background"> | ||
| <section className="w-[1040px] mx-auto pt-[80px]"> | ||
| <div className="flex items-start justify-between gap-10"> | ||
| <div className="flex-1"> | ||
| <AdminUserProfile /> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| </div> | ||
|
|
||
| <aside className="w-[520px]"> | ||
| <AdminCategory | ||
| selectedCategories={selectedCategories} | ||
| /> | ||
|
Comment on lines
+47
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| </aside> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col items-center w-full gap-[40px] mt-[80px]"> | ||
| <AdminUserTabs | ||
| activeTab={activeTab} | ||
| onTabChange={setActiveTab} | ||
| /> | ||
|
|
||
| {activeTab === "meetings" && <MeetingList />} | ||
| {activeTab === "stories" && <BookStoryList />} | ||
| {activeTab === "posts" && <NewsList />} | ||
| {activeTab === "reports" && <ReportList />} | ||
| </div> | ||
| </section> | ||
| </main> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| "use client"; | ||
|
|
||
| import { CATEGORIES } from "@/constants/categories"; | ||
|
|
||
| interface Props { | ||
| selectedCategories?: string[]; | ||
| } | ||
|
|
||
| export default function AdminCategory({ | ||
| selectedCategories = [], | ||
| }: Props) { | ||
| return ( | ||
| <div className="w-[526px]"> | ||
| {/* 4 x 4 grid */} | ||
| <div className="grid grid-cols-4 gap-x-[12px] gap-y-[8px]"> | ||
| {CATEGORIES.map((cat) => { | ||
| const isSelected = selectedCategories.includes(cat.value); | ||
|
|
||
| return ( | ||
| <div | ||
| key={cat.value} | ||
| className={` | ||
| w-[122px] h-[44px] | ||
| flex items-center justify-center gap-[8px] | ||
| px-[16px] py-[12px] | ||
| rounded-[400px] | ||
| border border-Subbrown-3 | ||
| transition-colors | ||
| ${isSelected ? "bg-Subbrown-1" : "bg-background"} | ||
| `} | ||
| > | ||
| <span | ||
| className={ | ||
| isSelected | ||
| ? "body_1_2 text-white" | ||
| : "body_1_3 text-Gray-5" | ||
| } | ||
| > | ||
| {cat.label} | ||
| </span> | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,66 @@ | ||||||||||
| "use client"; | ||||||||||
|
|
||||||||||
| import React from "react"; | ||||||||||
| import Image from "next/image"; | ||||||||||
| import { DUMMY_USER_PROFILE } from "@/constants/mocks/mypage"; | ||||||||||
| import { useProfileQuery } from "@/hooks/queries/useMemberQueries"; | ||||||||||
|
|
||||||||||
| const AdminUserProfile = () => { | ||||||||||
| const { data: profileData } = useProfileQuery(); | ||||||||||
|
|
||||||||||
| const user = { | ||||||||||
| ...DUMMY_USER_PROFILE, | ||||||||||
| name: profileData?.nickname || DUMMY_USER_PROFILE.name, | ||||||||||
| intro: profileData?.description || DUMMY_USER_PROFILE.intro, | ||||||||||
| profileImage: | ||||||||||
| profileData?.profileImageUrl || DUMMY_USER_PROFILE.profileImage, | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const email = (profileData as any)?.email || "example@email.com"; | ||||||||||
| const phone = (profileData as any)?.phone || "010-0000-0000"; | ||||||||||
|
Comment on lines
+19
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <div className="w-[479px] h-[212px]"> | ||||||||||
| <div className="grid grid-rows-[auto_1fr] h-full"> | ||||||||||
| {/* 1행 */} | ||||||||||
| <div className="flex items-start gap-[24px]"> | ||||||||||
| {/* 프로필 이미지 */} | ||||||||||
| <div className="w-[112px] h-[112px] rounded-full overflow-hidden relative shrink-0 bg-Subbrown-4"> | ||||||||||
| {user.profileImage ? ( | ||||||||||
| <Image | ||||||||||
| src={user.profileImage} | ||||||||||
| alt="Profile" | ||||||||||
| fill | ||||||||||
| className="object-cover" | ||||||||||
| /> | ||||||||||
| ) : ( | ||||||||||
| <div className="w-full h-full bg-Subbrown-4" /> | ||||||||||
| )} | ||||||||||
| </div> | ||||||||||
|
|
||||||||||
| {/* 정보 영역 */} | ||||||||||
| <dl className="grid grid-cols-[64px_1fr] gap-x-[18px] gap-y-[8px] pt-[6px]"> | ||||||||||
| <dt className="body_1_3 text-Gray-4">아이디</dt> | ||||||||||
| <dd className="body_1_2 text-Gray-7">{user.name}</dd> | ||||||||||
|
|
||||||||||
| <dt className="body_1_3 text-Gray-4">이름</dt> | ||||||||||
| <dd className="body_1_2 text-Gray-7">윤현일</dd> | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||
|
|
||||||||||
| <dt className="body_1_3 text-Gray-4">이메일</dt> | ||||||||||
| <dd className="body_1_2 text-Gray-7">{email}</dd> | ||||||||||
|
|
||||||||||
| <dt className="body_1_3 text-Gray-4">전화번호</dt> | ||||||||||
| <dd className="body_1_2 text-Gray-7">{phone}</dd> | ||||||||||
| </dl> | ||||||||||
| </div> | ||||||||||
|
|
||||||||||
| {/* 2행 소개글 */} | ||||||||||
| <p className="body_1_3 text-Gray-4 leading-[145%] mt-[12px] line-clamp-3"> | ||||||||||
| {user.intro} | ||||||||||
| </p> | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| ); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| export default AdminUserProfile; | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| "use client"; | ||
|
|
||
| import React from "react"; | ||
|
|
||
| export type AdminUserTabId = "meetings" | "stories" | "posts" | "reports"; | ||
|
|
||
| const USER_TABS: Array<{ id: AdminUserTabId; label: string }> = [ | ||
| { id: "meetings", label: "가입 모임" }, | ||
| { id: "stories", label: "책 이야기" }, | ||
| { id: "posts", label: "등록 소식" }, | ||
| { id: "reports", label: "신고 목록" }, | ||
| ]; | ||
|
|
||
| interface AdminUserTabsProps { | ||
| activeTab: AdminUserTabId; | ||
| onTabChange: (id: AdminUserTabId) => void; | ||
| } | ||
|
|
||
| const AdminUserTabs = ({ activeTab, onTabChange }: AdminUserTabsProps) => { | ||
| return ( | ||
| <div className="flex items-center w-full md:w-[768px] lg:w-[1440px] px-0 md:px-[60px] lg:px-[197px] border-b-2 border-Gray-2"> | ||
| {USER_TABS.map((tab) => ( | ||
| <button | ||
| key={tab.id} | ||
| onClick={() => onTabChange(tab.id)} | ||
| className={`flex-1 flex justify-center items-center gap-[10px] p-[10px] | ||
| body_1_2 md:subhead_3 | ||
| transition-colors border-b-2 ${ | ||
| activeTab === tab.id | ||
| ? "text-primary-3 border-primary-3 -mb-[2px]" | ||
| : "text-Gray-3 border-transparent" | ||
| }`} | ||
| > | ||
| {tab.label} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default AdminUserTabs; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| "use client"; | ||
|
|
||
| import React, { useEffect } from "react"; | ||
| import BookStoryCard from "./items/AdminBookStoryCard"; | ||
| import { useMyInfiniteStoriesQuery } from "@/hooks/queries/useStoryQueries"; | ||
| import { useInView } from "react-intersection-observer"; | ||
|
|
||
| const MyBookStoryList = () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| const { | ||
| data, | ||
| fetchNextPage, | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| isLoading, | ||
| isError, | ||
| } = useMyInfiniteStoriesQuery(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| const { ref, inView } = useInView(); | ||
|
|
||
| useEffect(() => { | ||
| if (inView && hasNextPage && !isFetchingNextPage) { | ||
| fetchNextPage(); | ||
| } | ||
| }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); | ||
|
|
||
| const stories = data?.pages.flatMap((page) => page.basicInfoList) || []; | ||
|
|
||
| return ( | ||
| <div className="flex flex-col items-center w-full max-w-[1048px] mx-auto gap-[20px] px-[18px] md:px-[40px] lg:px-0"> | ||
|
|
||
| {isLoading && <p className="text-Gray-4 text-center py-4">로딩 중...</p>} | ||
|
|
||
| {!isLoading && isError && ( | ||
| <p className="text-red-500 text-center py-4">책 이야기를 불러오는 데 실패했습니다.</p> | ||
| )} | ||
|
|
||
| {!isLoading && !isError && stories.length === 0 && ( | ||
| <p className="text-Gray-4 text-center py-4">작성한 책 이야기가 없습니다.</p> | ||
| )} | ||
|
|
||
| <div className="grid grid-cols-2 min-[540px]:grid-cols-3 md:grid-cols-2 lg:grid-cols-3 gap-[20px] md:gap-[12px] lg:gap-[20px] w-fit"> | ||
| {stories.map((story) => ( | ||
| <BookStoryCard | ||
| key={story.bookStoryId} | ||
| authorName={story.authorInfo.nickname} | ||
| createdAt={story.createdAt} | ||
| viewCount={story.viewCount} | ||
| title={story.bookStoryTitle} | ||
| content={story.description} | ||
| likeCount={story.likes} | ||
| commentCount={story.commentCount} | ||
| coverImgSrc={story.bookInfo.imgUrl} | ||
| profileImgSrc={story.authorInfo.profileImageUrl} | ||
| hideSubscribeButton={true} | ||
| /> | ||
| ))} | ||
| </div> | ||
|
|
||
| {/* Infinite Scroll Trigger */} | ||
| <div ref={ref} className="h-4 w-full" /> | ||
|
|
||
| {isFetchingNextPage && ( | ||
| <p className="text-Gray-4 text-center py-4">추가 이야기를 불러오는 중...</p> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default MyBookStoryList; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| "use client"; | ||
|
|
||
| import React from "react"; | ||
| import MyMeetingCard from "./items/AdminMeetingCard"; | ||
| import { useMyClubsQuery } from "@/hooks/queries/useClubQueries"; | ||
|
|
||
| const MyMeetingList = () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| const { data, isLoading, isError } = useMyClubsQuery(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| const clubs = data?.clubList || []; | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center py-10 w-full text-Gray-4 text-sm font-medium"> | ||
| 불러오는 중... | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (isError) { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center py-10 w-full text-red-500 text-sm font-medium"> | ||
| 독서 모임을 불러오는 데 실패했습니다. | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (clubs.length === 0) { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center py-20 w-full"> | ||
| <p className="text-Gray-4 text-sm font-medium whitespace-pre-wrap text-center"> | ||
| 가입한 독서 모임이 없습니다. | ||
| </p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex flex-col items-start gap-[8px] w-full max-w-[1048px] px-[18px] md:px-[40px] lg:px-0 mx-auto"> | ||
| {clubs.map((club) => ( | ||
| <MyMeetingCard key={club.clubId} club={club} /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default MyMeetingList; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // NewsList.tsx | ||
| "use client"; | ||
| import NewsItem from "./items/AdminNewsItem"; | ||
|
|
||
| export default function NewsList() { | ||
| const posts = [ | ||
| { id: 1, imageUrl: "/...", title: "...", content: "...", date: "2025.01.01" }, | ||
| ]; | ||
|
Comment on lines
+6
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| return ( | ||
| <div className="flex flex-col gap-[16px] w-full max-w-[1040px] mx-auto"> | ||
| {posts.map(post => ( | ||
| <NewsItem key={post.id} {...post} /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재
user객체가 더미 데이터로 하드코딩되어 있습니다. PR 설명에 따라 API 연동 후 실제 사용자 데이터를 기반으로 이 부분을 대체해야 합니다.[id]라우트 파라미터를 활용하여 특정 사용자의 정보를 가져오도록 구현해야 합니다.