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
20 changes: 20 additions & 0 deletions app/(service)/(my)/my-study/entry-list/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import EntryList from '@/features/my-page/ui/entry-list';
import Image from 'next/image';

export default function EntryPage() {
return (
<div className="w-720px flex flex-col gap-300">
<div className="font-designer-20b flex items-center">
<Image
src="/icons/arrow-left-line.svg"
alt="arrow-left"
width={40}
height={40}
/>
<div className="flex-1 text-center">새로운 신청자 확인하기</div>
</div>

<EntryList />
</div>
);
}
3 changes: 3 additions & 0 deletions app/(service)/(nav)/study/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function StudyPage() {
return <div>Study Page</div>;
}
28 changes: 28 additions & 0 deletions app/(service)/(nav)/study/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import GroupStudyList from '@/features/study/group/ui/group-study-list';
import IconPlus from '@/shared/icons/plus.svg';
import Button from '@/shared/ui/button';
import Sidebar from '@/widgets/home/sidebar';

export default function Study() {
return (
<div className="flex w-full gap-600 py-600">
<div className="flex flex-1 flex-col gap-500">
<div className="flex justify-between">
<span className="font-designer-28b text-[#181D27]">
스터디 둘러보기
</span>
<Button
color="primary"
size="large"
iconPosition="left"
icon={<IconPlus />}
>
스터디 개설하기
</Button>
</div>
<GroupStudyList />
</div>
<Sidebar />
</div>
);
}
4 changes: 2 additions & 2 deletions app/(service)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export default function ServiceLayout({
<MainProvider>
<div className="w-full overflow-auto">
{/** 1400 + 48*2 패딩 양옆 48로 임의적용 */}
<div className="m-auto flex w-[1496px] flex-1 flex-col items-center px-600">
<div className="m-auto flex w-[1496px] flex-1 flex-col items-center">
<Header />
<main className="w-full">{children}</main>
<main className="w-full p-600">{children}</main>
</div>
</div>
</MainProvider>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.18",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.475.0",
"next": "15.1.7",
Expand Down
3 changes: 3 additions & 0 deletions public/icons/arrow-left-line.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions src/features/my-page/api/get-entry-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { axiosInstance } from '@/shared/tanstack-query/axios';
import { EntryListRequest, GroupStudyApplyListResponse } from './types';

// 그룹 스터디 리스트 조회
export const getEntryList = async (
params: EntryListRequest,
): Promise<GroupStudyApplyListResponse> => {
const { page, size, status, groupStudyId } = params;

const { data } = await axiosInstance.get(
`/group-studies/${groupStudyId}/applies?applyStatus=${status}`,
{
params: {
page,
size,
},
},
);

if (data.statusCode !== 200) {
throw new Error('Failed to fetch entry list');
}

return data.content;
};
77 changes: 77 additions & 0 deletions src/features/my-page/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,80 @@ export interface StudyDashboardResponse {
studyActivity: StudyActivity;
growthMetric: GrowthMetric;
}

export interface EntryListRequest {
groupStudyId: number;
page: number;
size: number;
status?: 'PENDING';
}

export interface EntryStatusRequest {
groupStudyId: number;
applyId: number;
status: 'APPROVED' | 'REJECTED';
}

export interface ImageSizeType {
imageTypeName: 'ORIGINAL' | 'SMALL' | 'MEDIUM' | 'LARGE';
width: number | null;
height: number | null;
}

export interface ResizedImage {
resizedImageId: number;
resizedImageUrl: string;
imageSizeType: ImageSizeType;
}

export interface ProfileImage {
imageId: number;
resizedImages: ResizedImage[];
}

export interface SincerityTemp {
temperature: number;
levelId: number;
levelName: '1단계' | '2단계' | '3단계' | '4단계'; // 추후 서버 기준에 맞게 확장 가능
}

export interface Applicant {
memberId: number;
memberName: string;
profileImage: ProfileImage;
sincerityTemp: SincerityTemp;
}

export interface GroupStudy {
groupStudyId: number;
title: string;
description: string;
}

export type ApplyRole = 'LEADER' | 'PARTICIPANT';
export type ApplyStatus = 'PENDING' | 'APPROVED' | 'REJECTED';

export interface GroupStudyApply {
applyId: number;
applicantInfo: Applicant;
groupStudy: GroupStudy;
progressScore: number;
role: ApplyRole;
lastAccessed: string;
answer: string[];
status: ApplyStatus;
processedAt: string | null;
reason: string | null;
createdAt: string;
updatedAt: string;
}

export interface GroupStudyApplyListResponse {
content: GroupStudyApply[];
page: number;
size: number;
totalElements: number;
totalPages: number;
hasNext: boolean;
hasPrevious: boolean;
}
22 changes: 22 additions & 0 deletions src/features/my-page/api/update-entry-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { axiosInstance } from '@/shared/tanstack-query/axios';
import { EntryStatusRequest } from './types';

// 그룹 스터디 리스트 조회
export const updateEntryStatus = async (params: EntryStatusRequest) => {
const { status, groupStudyId, applyId } = params;

const { data } = await axiosInstance.patch(
`/group-studies/${groupStudyId}/apply/${applyId}/process`,
{
params: {
status,
},
},
);

if (data.statusCode !== 200) {
throw new Error('Failed to fetch entry list');
}

return data.content;
};
89 changes: 89 additions & 0 deletions src/features/my-page/ui/entry-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import dayjs from 'dayjs';
import Image from 'next/image';
import React from 'react';
import { getSincerityPresetByLevelName } from '@/shared/config/sincerity-temp-presets';
import { cn } from '@/shared/shadcn/lib/utils';
import Button from '@/shared/ui/button';
import { GroupStudyApply } from '../api/types';

export default function EntryCard(props: { data: GroupStudyApply }) {
const { data: applicant } = props;
const temperPreset = getSincerityPresetByLevelName(
applicant.applicantInfo.sincerityTemp.levelName as string,
);

const timeAgo = (date: string | Date): string => {
const now = dayjs();
const target = dayjs(date);

if (!target.isValid()) return '';

const diffMin = now.diff(target, 'minute');
if (diffMin < 1) return '방금 전';
if (diffMin < 60) return `${diffMin}분 전`;

const diffHour = now.diff(target, 'hour');
if (diffHour < 24) return `${diffHour}시간 전`;

const diffDay = now.diff(target, 'day');
if (diffDay < 7) return `${diffDay}일 전`;

const diffWeek = Math.floor(diffDay / 7);

return `${diffWeek}주 전`;
};

const ApplicantStatus = () => {};

return (
<div className="rounded-100 flex w-full cursor-pointer flex-col gap-150 border border-[#E9EAEB] p-300">
<div className="flex gap-150">
<Image
src="/profile-default.jpg"
alt="profile"
width={48}
height={48}
/>
<div>
<div className="flex items-center gap-50">
<span className="font-designer-14b">
{applicant.applicantInfo.memberName}
</span>
<span
className={cn(
'font-designer-13r rounded-full px-150 py-50 leading-250',
temperPreset.bgClass,
temperPreset.textClass,
)}
>
{`${applicant.applicantInfo.sincerityTemp.temperature}`} ℃
</span>
</div>

<p className="font-designer-13r text-text-subtle">
{timeAgo(applicant.createdAt)}
</p>
</div>
</div>
<p className="font-designer-16r text-text-default">{applicant.answer}</p>
<div className="flex w-full justify-end gap-100">
<Button
size="medium"
type="button"
color="secondary"
className="w-[120px]"
>
반려
</Button>
<Button
size="medium"
type="button"
color="primary"
className="w-[120px]"
>
승인
</Button>
</div>
</div>
);
}
44 changes: 44 additions & 0 deletions src/features/my-page/ui/entry-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';
import { useInfiniteQuery } from '@tanstack/react-query';
import React from 'react';
import EntryCard from './entry-card';
import { getEntryList } from '../api/get-entry-list';

export default function EntryList() {
const { data, fetchNextPage } = useInfiniteQuery({
queryKey: ['entryList'],
queryFn: async ({ pageParam }) => {
const response = await getEntryList({
groupStudyId: 1,
page: pageParam,
size: 20,
status: 'PENDING',
});

return response;
},
getNextPageParam: (lastPage) => {
if (lastPage.hasNext) {
return lastPage.page + 1;
}

return null;
},
initialPageParam: 0,
maxPages: 3,
});

console.log('data', data);

return (
<div className="flex w-full flex-col gap-500">
{data?.pages.map((page, pageIndex) => (
<React.Fragment key={pageIndex}>
{page.content.map((applicant) => (
<EntryCard key={applicant.applyId} data={applicant} />
))}
</React.Fragment>
))}
</div>
);
}
28 changes: 28 additions & 0 deletions src/features/study/group/api/get-group-study-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { axiosInstance } from '@/shared/tanstack-query/axios';
import {
GroupStudyListRequest,
GroupStudyListResponse,
} from './group-study-types';

// 그룹 스터디 리스트 조회
export const getGroupStudyList = async (
params: GroupStudyListRequest,
): Promise<GroupStudyListResponse> => {
const { page, size, status } = params;

try {
const { data } = await axiosInstance.get('/group-studies', {
params: {
page,
size,
status,
},
});

if (data.statusCode !== 200) {
throw new Error('Failed to fetch group study list');
}

return data.content;
} catch (err) {}
};
Loading