diff --git a/public/icons/silver-rank.svg b/public/icons/silver-rank.svg
new file mode 100644
index 00000000..1a061f3e
--- /dev/null
+++ b/public/icons/silver-rank.svg
@@ -0,0 +1,21 @@
+
diff --git a/src/features/admin/ui/member-list-table.tsx b/src/features/admin/ui/member-list-table.tsx
index d0623474..40ed0c79 100644
--- a/src/features/admin/ui/member-list-table.tsx
+++ b/src/features/admin/ui/member-list-table.tsx
@@ -202,7 +202,13 @@ export default function MemberListTable() {
: '-'}
|
- {user.role.roleId === 'ROLE_MENTOR' && }
+ {user.role.roleId === 'ROLE_MENTOR' && (
+
+ )}
{ROLE_MAP[user.role.roleId]}
|
diff --git a/src/features/study/group/api/get-group-study-member-list.ts b/src/features/study/group/api/get-group-study-member-list.ts
new file mode 100644
index 00000000..54755338
--- /dev/null
+++ b/src/features/study/group/api/get-group-study-member-list.ts
@@ -0,0 +1,19 @@
+import { axiosInstance } from '@/shared/tanstack-query/axios';
+import {
+ GroupStudyMembersRequest,
+ GroupStudyMembersResponse,
+} from './group-study-types';
+
+// 그룹 스터디 참여자 리스트 조회 API - 페이징 처리 o/x 둘 다 가능
+export const getGroupStudyMemberList = async ({
+ id,
+ pageNumber,
+ pageSize,
+}: GroupStudyMembersRequest): Promise => {
+ const isPaging = !!(pageNumber && pageSize);
+ const res = await axiosInstance.get(`/group-studies/${id}/members`, {
+ params: isPaging ? { pageNumber, pageSize, isPaging } : { isPaging },
+ });
+
+ return res.data;
+};
diff --git a/src/features/study/group/api/group-study-types.ts b/src/features/study/group/api/group-study-types.ts
index 578f1ede..c4b8fae7 100644
--- a/src/features/study/group/api/group-study-types.ts
+++ b/src/features/study/group/api/group-study-types.ts
@@ -162,3 +162,48 @@ export interface GroupStudyDetailResponse {
detailInfo: DetailInfoDetail; // DetailInfo 확장
interviewPost: InterviewPostDetail; // InterviewPost 확장
}
+
+// 그룹 스터디 참여자 목록 API 타입
+export interface GroupStudyMembersRequest {
+ id: number;
+ pageNumber?: number;
+ pageSize?: number;
+}
+
+export interface GroupStudyMembersResponse {
+ pageSize: number;
+ pageNumber: number;
+ totalCount: number;
+ hasPrevious: boolean;
+ hasNext: boolean;
+ members: GroupStudyMember[];
+}
+
+export interface GroupStudyMember {
+ id: number;
+ profileImageUrl: string | null;
+ memberName: string;
+ progress: MemberProgress;
+ ranking: number;
+ greeting: string | null;
+ lastAccessedAt: string; // ISO datetime string
+}
+
+export interface MemberProgress {
+ score: number;
+ progressHistory: ProgressHistoryItem[];
+}
+
+export interface ProgressHistoryItem {
+ id: number;
+ acquiredAt: string; // ISO datetime string
+ grade: Grade;
+ reason: string;
+}
+
+export interface Grade {
+ id: number;
+ code: 'A+' | 'A-' | 'B+' | 'B-' | 'C+' | 'C-' | 'F'; // e.g. "A+", "C+"
+ name: string; // e.g. "Great", "Cheer up"
+ score: 4.5 | 4 | 3.5 | 3 | 2.5 | 2 | 0;
+}
diff --git a/src/features/study/group/model/use-group-study-member-list-query.ts b/src/features/study/group/model/use-group-study-member-list-query.ts
new file mode 100644
index 00000000..34edc268
--- /dev/null
+++ b/src/features/study/group/model/use-group-study-member-list-query.ts
@@ -0,0 +1,15 @@
+import { useQuery } from '@tanstack/react-query';
+import { getGroupStudyMemberList } from '../api/get-group-study-member-list';
+import { GroupStudyMembersRequest } from '../api/group-study-types';
+
+export const useGroupStudyMemberListQuery = ({
+ id,
+ pageNumber,
+ pageSize,
+}: GroupStudyMembersRequest) => {
+ return useQuery({
+ queryKey: ['groupStudyMemberList', id, pageNumber, pageSize],
+ queryFn: () => getGroupStudyMemberList({ id, pageNumber, pageSize }),
+ enabled: !!id,
+ });
+};
diff --git a/src/features/study/group/ui/group-study-member-item.tsx b/src/features/study/group/ui/group-study-member-item.tsx
new file mode 100644
index 00000000..257e08e7
--- /dev/null
+++ b/src/features/study/group/ui/group-study-member-item.tsx
@@ -0,0 +1,222 @@
+'use client';
+
+import Image from 'next/image';
+import { useState } from 'react';
+import {
+ formatHHMM,
+ formatKoreaRelativeTime,
+ formatYYYYMMDD,
+} from '@/shared/lib/time';
+import { getCookie } from '@/shared/tanstack-query/cookie';
+import Button from '@/shared/ui/button';
+import BronzeRankIcon from 'public/icons/bronze-rank.svg';
+import CaretDownIcon from 'public/icons/caret-down.svg';
+import CaretUpIcon from 'public/icons/caret-up.svg';
+import GoldRankIcon from 'public/icons/gold-rank.svg';
+import SealCheckIcon from 'public/icons/seal-check.svg';
+import SilverRankIcon from 'public/icons/silver-rank.svg';
+import {
+ GroupStudyMember,
+ ProgressHistoryItem,
+} from '../api/group-study-types';
+
+export default function GroupStudyMemberItem(member: GroupStudyMember) {
+ const [isProgressHistoryOpen, setIsProgressHistoryOpen] =
+ useState(false);
+
+ const isMe = member.id === Number(getCookie('memberId'));
+
+ return (
+
+
+ {/* 사용자 프로필 */}
+
+
+
+ {/* 랭크 아이콘 */}
+ {[1, 2, 3].includes(member.ranking) && (
+
+ {member.ranking === 1 && }
+ {member.ranking === 2 && }
+ {member.ranking === 3 && }
+
+ )}
+
+
+
+
+ {member.memberName}
+
+
+ 최근 접속 시간: {formatKoreaRelativeTime(member.lastAccessedAt)}
+
+
+
+
+
+
+ 가입 인사
+
+
+
+
+
+
+
+
+
+
+ 스터디 진행도
+
+
+ {member.progress.score}%
+
+
+
+
+
+
+
+
+ {isProgressHistoryOpen && (
+
+ {member.progress.progressHistory.map((history) => (
+
+ ))}
+
+ )}
+
+
+
+ );
+}
+
+function ProgressBar({
+ value,
+ className = '',
+}: {
+ value: number;
+ className?: string;
+}) {
+ const clamped = Math.max(0, Math.min(100, Number(value) || 0));
+
+ return (
+
+ );
+}
+
+const renderGradeIcon = (code: ProgressHistoryItem['grade']['code']) => {
+ switch (code) {
+ case 'A+':
+ case 'A-':
+ return (
+
+
+
+ );
+ case 'B+':
+ case 'B-':
+ return (
+
+
+
+ );
+ case 'C+':
+ case 'C-':
+ return (
+
+
+
+ );
+ default:
+ return null;
+ }
+};
+
+function ProgressScoreItem({
+ grade,
+ reason,
+ acquiredAt,
+ id,
+}: ProgressHistoryItem) {
+ return (
+
+ {renderGradeIcon(grade.code)}
+
+
+
+
+ {grade.code} : {grade.name} (
+ {`${grade.score > 0 ? '+' : ''}${grade.score}`})
+
+
+
+
+ {reason}
+
+
+ {`${formatYYYYMMDD(acquiredAt, 'dot')} ${formatHHMM(acquiredAt)}`}
+
+
+ );
+}
+
+function GreetingBox({
+ id,
+ greeting,
+}: Pick) {
+ // 가입인사를 작성한 경우
+ if (greeting) {
+ return {greeting} ;
+ }
+
+ // 가입인사를 작성하지 못한 경우
+ const isMe = id === Number(getCookie('memberId'));
+
+ if (isMe) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ 가입 인사를 작성하지 않았습니다.
+
+ );
+}
diff --git a/src/shared/lib/time.ts b/src/shared/lib/time.ts
index 704f878d..e2d8feb3 100644
--- a/src/shared/lib/time.ts
+++ b/src/shared/lib/time.ts
@@ -20,10 +20,13 @@ export const getKoreaDate = (targetDate?: Date) => {
return koreaNow;
};
-export const formatYYYYMMDD = (dateString: string) => {
+export const formatYYYYMMDD = (
+ dateString: string,
+ separator: 'dash' | 'dot' = 'dash',
+) => {
const onlyDate = new Date(dateString).toISOString().slice(0, 10);
- return onlyDate;
+ return onlyDate.replace(/-/g, separator === 'dash' ? '-' : '.');
};
export const formatHHMM = (dateString: string) => {
|