diff --git a/app/(my)/my-study-review/page.tsx b/app/(my)/my-study-review/page.tsx index 21cffeda..23c2bf88 100644 --- a/app/(my)/my-study-review/page.tsx +++ b/app/(my)/my-study-review/page.tsx @@ -2,16 +2,16 @@ import Image from 'next/image'; import { useEffect, useRef, useState } from 'react'; +import { MyReviewItem } from '@/entities/review/api/review-types'; import KeywordReview from '@/entities/user/ui/keyword-review'; import MoreKeywordReviewModal from '@/entities/user/ui/more-keyword-review-modal'; -import { MyReviewItem } from '@/features/study/api/types'; +import { formatKoreaRelativeTime } from '@/shared/lib/time'; +import UserAvatar from '@/shared/ui/avatar'; import { useMyNegativeKeywordsQuery, useMyReviewsInfinityQuery, useUserPositiveKeywordsQuery, -} from '@/features/study/model/use-review-query'; -import { formatKoreaRelativeTime } from '@/shared/lib/time'; -import UserAvatar from '@/shared/ui/avatar'; +} from '@/entities/review/model/use-review-query'; export default function MyStudyReview() { const { data: positiveKeywordsData } = useUserPositiveKeywordsQuery({ diff --git a/app/page.tsx b/app/page.tsx index 80303a27..0f443b24 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,5 @@ import { Metadata } from 'next'; -import StudyCard from '@/features/study/ui/study-card'; +import StudyCard from '@/features/study/schedule/ui/study-card'; import Banner from '@/widgets/home/banner'; import Sidebar from '@/widgets/home/sidebar'; diff --git a/src/features/study/api/get-review.ts b/src/entities/review/api/get-review.ts similarity index 97% rename from src/features/study/api/get-review.ts rename to src/entities/review/api/get-review.ts index 58b3a4cb..a53c290e 100644 --- a/src/features/study/api/get-review.ts +++ b/src/entities/review/api/get-review.ts @@ -1,4 +1,3 @@ -import { axiosInstance } from '@/shared/tanstack-query/axios'; import type { AddStudyReviewRequest, UserPositiveKeywordsResponse, @@ -9,7 +8,8 @@ import type { MyReviewsResponse, MyReviewsRequest, ShouldReviewPartnerResponse, -} from './types'; +} from '@/entities/review/api/review-types'; +import { axiosInstance } from '@/shared/tanstack-query/axios'; export const getPartnerStudyReview = async (): Promise => { diff --git a/src/features/study/api/types.ts b/src/entities/review/api/review-types.ts similarity index 51% rename from src/features/study/api/types.ts rename to src/entities/review/api/review-types.ts index 70fd16c9..20d478cc 100644 --- a/src/features/study/api/types.ts +++ b/src/entities/review/api/review-types.ts @@ -1,100 +1,4 @@ -export type StudyProgressStatus = - | 'PENDING' - | 'IN_PROGRESS' - | 'COMPLETE' - | 'ABSENT'; - -export interface DailyStudy { - interviewer: string; - interviewerImage: string; - interviewee: string; - intervieweeImage: string; - dailyStudyId: number; - subject: string; - description: string; - link: string; - progressStatus: StudyProgressStatus; - studyDate: string; - feedback: string | undefined; -} - -export interface DailyStudyDetail { - dailyStudyId: number; - interviewerId: number; - interviewerName: string; - interviewerImage: string; - intervieweeId: number; - intervieweeName: string; - intervieweeImage: string; - studySpaceId: number; - progressStatus: StudyProgressStatus; - subject: string; - description: string; - link: string; - feedback: string; -} - -export interface GetDailyStudiesParams { - cursor?: number; - pageSize?: number; - studyDate?: string; -} - -export interface GetDailyStudiesResponse { - items: DailyStudy[]; - nextCursor: number; - hasNext: boolean; -} - -export interface GetMonthlyCalendarParams { - year: number; - month: number; -} - -export interface StudyCalendarDay { - day: number; - hasStudy: boolean; - status: StudyProgressStatus | undefined; -} - -export interface MonthlyCalendarResponse { - calendar: StudyCalendarDay[]; - monthlyCompletedCount?: number; - totalCompletedCount?: number; -} - -export interface PostDailyRetrospectRequest { - description: string; - parentId: number; -} - -export interface PrepareStudyRequest { - subject: string; - link: string; -} - -export interface JoinStudyRequest { - memberId: number; - selfIntroduction?: string; - studyPlan?: string; - preferredStudySubjectId?: string; - availableStudyTimeIds?: number[]; - techStackIds?: number[]; - tel?: string; - githubLink?: string; - blogOrSnsLink?: string; -} - -export interface WeeklyParticipationResponse { - memberId: number; - isParticipate: boolean; -} - -export interface CompleteStudyRequest { - feedback: string; - progressStatus: StudyProgressStatus; -} - +// 리뷰 관련 타입 export interface EvalKeyword { id: number; keyword: string; diff --git a/src/features/study/lib/use-reminder-review.tsx b/src/entities/review/lib/use-reminder-review.tsx similarity index 100% rename from src/features/study/lib/use-reminder-review.tsx rename to src/entities/review/lib/use-reminder-review.tsx diff --git a/src/features/study/model/use-review-query.ts b/src/entities/review/model/use-review-query.ts similarity index 98% rename from src/features/study/model/use-review-query.ts rename to src/entities/review/model/use-review-query.ts index fe2fbfc7..26d994a0 100644 --- a/src/features/study/model/use-review-query.ts +++ b/src/entities/review/model/use-review-query.ts @@ -4,6 +4,10 @@ import { useQuery, useSuspenseQuery, } from '@tanstack/react-query'; +import { + MyNegativeKeywordsRequest, + UserPositiveKeywordsRequest, +} from '@/entities/review/api/review-types'; import { getKoreaDate } from '@/shared/lib/time'; import { addStudyReview, @@ -13,10 +17,6 @@ import { getMyReviews, getShouldReviewPartner, } from '../api/get-review'; -import { - MyNegativeKeywordsRequest, - UserPositiveKeywordsRequest, -} from '../api/types'; export const usePartnerStudyReviewQuery = () => { return useSuspenseQuery({ diff --git a/src/features/study/ui/study-review-modal.tsx b/src/entities/review/ui/study-review-modal.tsx similarity index 98% rename from src/features/study/ui/study-review-modal.tsx rename to src/entities/review/ui/study-review-modal.tsx index 4bfd7ace..e5f7a0f8 100644 --- a/src/features/study/ui/study-review-modal.tsx +++ b/src/entities/review/ui/study-review-modal.tsx @@ -3,17 +3,20 @@ import { XIcon } from 'lucide-react'; import Image from 'next/image'; import { useState } from 'react'; +import { + EvalKeyword, + StudyEvaluationResponse, +} from '@/entities/review/api/review-types'; import UserAvatar from '@/shared/ui/avatar'; import Button from '@/shared/ui/button'; import Checkbox from '@/shared/ui/checkbox'; import { TextAreaInput } from '@/shared/ui/input'; import ListItem from '@/shared/ui/list-item'; import { Modal } from '@/shared/ui/modal'; -import { EvalKeyword, StudyEvaluationResponse } from '../api/types'; import { useAddStudyReviewMutation, usePartnerStudyReviewQuery, -} from '../model/use-review-query'; +} from '@/entities/review/model/use-review-query'; interface FormState { studySpaceId: number; diff --git a/src/entities/user/model/use-user-profile-query.ts b/src/entities/user/model/use-user-profile-query.ts index d31a5f19..38d00741 100644 --- a/src/entities/user/model/use-user-profile-query.ts +++ b/src/entities/user/model/use-user-profile-query.ts @@ -42,7 +42,7 @@ export const usePatchAutoMatchingMutation = () => { }); } - return { prev }; + return { prev }; }, onError: (_err, { memberId }, ctx) => { diff --git a/src/entities/user/ui/my-profile-card.tsx b/src/entities/user/ui/my-profile-card.tsx index cd2eac19..606920eb 100644 --- a/src/entities/user/ui/my-profile-card.tsx +++ b/src/entities/user/ui/my-profile-card.tsx @@ -2,9 +2,9 @@ import Link from 'next/link'; import React, { useState } from 'react'; +import { useReviewReminder } from '@/entities/review/lib/use-reminder-review'; +import StudyReviewModal from '@/entities/review/ui/study-review-modal'; import { usePatchAutoMatchingMutation } from '@/entities/user/model/use-user-profile-query'; -import { useReviewReminder } from '@/features/study/lib/use-reminder-review'; -import StudyReviewModal from '@/features/study/ui/study-review-modal'; import { getSincerityPresetByLevelName } from '@/shared/config/sincerity-temp-presets'; import { cn } from '@/shared/shadcn/lib/utils'; import UserAvatar from '@/shared/ui/avatar'; diff --git a/src/entities/user/ui/user-profile-modal.tsx b/src/entities/user/ui/user-profile-modal.tsx index 041aaa84..9385fbe2 100644 --- a/src/entities/user/ui/user-profile-modal.tsx +++ b/src/entities/user/ui/user-profile-modal.tsx @@ -9,11 +9,11 @@ import CakeIcon from '@/features/my-page/ui/icon/cake.svg'; import GithubIcon from '@/features/my-page/ui/icon/github-logo.svg'; import GlobeIcon from '@/features/my-page/ui/icon/globe-simple.svg'; import PhoneIcon from '@/features/my-page/ui/icon/phone.svg'; -import { useUserPositiveKeywordsQuery } from '@/features/study/model/use-review-query'; import { getSincerityPresetByLevelName } from '@/shared/config/sincerity-temp-presets'; import UserAvatar from '@/shared/ui/avatar'; import Badge from '@/shared/ui/badge'; import { Modal } from '@/shared/ui/modal'; +import { useUserPositiveKeywordsQuery } from '@/entities/review/model/use-review-query'; interface UserProfileModalProps { memberId: number; diff --git a/src/features/my-page/model/profile-form.schema.ts b/src/features/my-page/model/profile-form.schema.ts index 51eee97d..5323a581 100644 --- a/src/features/my-page/model/profile-form.schema.ts +++ b/src/features/my-page/model/profile-form.schema.ts @@ -1,4 +1,3 @@ -import { FieldNamesMarkedBoolean } from 'react-hook-form'; import { z } from 'zod'; import type { MemberProfile } from '@/entities/user/api/types'; import { UrlSchema } from '@/shared/util/zod-schema'; diff --git a/src/features/my-page/ui/profile-edit-modal.tsx b/src/features/my-page/ui/profile-edit-modal.tsx index d6126b95..7d14986c 100644 --- a/src/features/my-page/ui/profile-edit-modal.tsx +++ b/src/features/my-page/ui/profile-edit-modal.tsx @@ -94,7 +94,7 @@ function ProfileEditForm({ const { handleSubmit, - formState: { isValid, isSubmitting, dirtyFields }, + formState: { isValid, isSubmitting }, } = methods; const onValidSubmit = async (values: ProfileFormValues) => { diff --git a/src/features/study/api/get-study-data.ts b/src/features/study/api/get-study-data.ts deleted file mode 100644 index 89f1c674..00000000 --- a/src/features/study/api/get-study-data.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { - CompleteStudyRequest, - DailyStudyDetail, - GetDailyStudiesParams, - GetDailyStudiesResponse, - GetMonthlyCalendarParams, - JoinStudyRequest, - MonthlyCalendarResponse, - PostDailyRetrospectRequest, - PrepareStudyRequest, - WeeklyParticipationResponse, -} from '@/features/study/api/types'; -import { axiosInstance } from '@/shared/tanstack-query/axios'; - -// 스터디 상세 조회 -export const getDailyStudyDetail = async ( - params: string, -): Promise => { - const res = await axiosInstance.get(`/study/daily/mine/${params}`); - - return res.data.content; -}; - -// 스터디 전체 조회 -export const getDailyStudies = async ( - params?: GetDailyStudiesParams, -): Promise => { - const res = await axiosInstance.get('/study/daily', { params }); - - return res.data.content; -}; - -// 월 별 스터디 캘린더 조회 -export const getMonthlyStudyCalendar = async ( - params: GetMonthlyCalendarParams, -): Promise => { - const res = await axiosInstance.get('/study/daily/month', { params }); - - return res.data.content; -}; - -export const postDailyRetrospect = async (body: PostDailyRetrospectRequest) => { - const res = await axiosInstance.post('/study/daily/retrospect', body); - - return res.data; -}; - -// 면접 준비 시작 -export const putStudyDaily = async ( - dailyId: number, - body: PrepareStudyRequest, -) => { - const res = await axiosInstance.put(`/study/daily/${dailyId}/prepare`, body); - - return res.data; -}; - -// 면접 완료 및 회고 작성 -export const completeStudy = async ( - dailyStudyId: number, - body: CompleteStudyRequest, -) => { - const res = await axiosInstance.post( - `/study/daily/${dailyStudyId}/complete`, - body, - ); - - return res.data; -}; - -// CS 스터디 매칭 신청 -export const postJoinStudy = async (payload: JoinStudyRequest) => { - const cleanPayload = Object.fromEntries( - Object.entries(payload).filter( - ([_, value]) => - value !== undefined && - value !== '' && - !(Array.isArray(value) && value.length === 0), - ), - ); - - const res = await axiosInstance.post('/matching/apply', cleanPayload); - - return res.data; -}; - -// 스터디 참여 유무 확인 -export const getWeeklyParticipation = async ( - studyDate: string, -): Promise => { - const res = await axiosInstance.get('/study/week/participation', { - params: { studyDate }, - }); - - return res.data.content; -}; diff --git a/src/features/study/interview/api/get-interview.ts b/src/features/study/interview/api/get-interview.ts new file mode 100644 index 00000000..c5feef00 --- /dev/null +++ b/src/features/study/interview/api/get-interview.ts @@ -0,0 +1,38 @@ +import type { + CompleteStudyRequest, + DailyStudyDetail, + PrepareStudyRequest, +} from '@/features/study/interview/api/interview-types'; +import { axiosInstance } from '@/shared/tanstack-query/axios'; + +// 스터디 상세 조회 +export const getDailyStudyDetail = async ( + params: string, +): Promise => { + const res = await axiosInstance.get(`/study/daily/mine/${params}`); + + return res.data.content; +}; + +// 면접 준비 시작 +export const putStudyDaily = async ( + dailyId: number, + body: PrepareStudyRequest, +) => { + const res = await axiosInstance.put(`/study/daily/${dailyId}/prepare`, body); + + return res.data; +}; + +// 면접 완료 및 회고 작성 +export const completeStudy = async ( + dailyStudyId: number, + body: CompleteStudyRequest, +) => { + const res = await axiosInstance.post( + `/study/daily/${dailyStudyId}/complete`, + body, + ); + + return res.data; +}; diff --git a/src/features/study/interview/api/interview-types.ts b/src/features/study/interview/api/interview-types.ts new file mode 100644 index 00000000..1e4a678e --- /dev/null +++ b/src/features/study/interview/api/interview-types.ts @@ -0,0 +1,34 @@ +export type StudyProgressStatus = + | 'PENDING' + | 'IN_PROGRESS' + | 'COMPLETE' + | 'ABSENT'; + +// 오늘의 스터디 상세조회 관련 타입 +export interface DailyStudyDetail { + dailyStudyId: number; + interviewerId: number; + interviewerName: string; + interviewerImage: string; + intervieweeId: number; + intervieweeName: string; + intervieweeImage: string; + studySpaceId: number; + progressStatus: StudyProgressStatus; + subject: string; + description: string; + link: string; + feedback: string; +} + +// 스터디 면접 준비 타입 +export interface PrepareStudyRequest { + subject: string; + link: string; +} + +// 스터디 면접 완료 타입 +export interface CompleteStudyRequest { + feedback: string; + progressStatus: StudyProgressStatus; +} diff --git a/src/features/study/interview/const/interview-const.ts b/src/features/study/interview/const/interview-const.ts new file mode 100644 index 00000000..69c334e1 --- /dev/null +++ b/src/features/study/interview/const/interview-const.ts @@ -0,0 +1,6 @@ +export const STUDY_PROGRESS_OPTIONS = [ + { label: '시작 전', value: 'PENDING' }, + { label: '불참', value: 'ABSENT' }, + { label: '진행중', value: 'IN_PROGRESS' }, + { label: '완료', value: 'COMPLETE' }, +]; diff --git a/src/features/study/model/interview.schema.ts b/src/features/study/interview/model/interview.schema.ts similarity index 85% rename from src/features/study/model/interview.schema.ts rename to src/features/study/interview/model/interview.schema.ts index 4f8bef55..041d01c1 100644 --- a/src/features/study/model/interview.schema.ts +++ b/src/features/study/interview/model/interview.schema.ts @@ -1,7 +1,10 @@ import { z } from 'zod'; +import type { + DailyStudyDetail, + StudyProgressStatus, +} from '@/features/study/interview/api/interview-types'; +import { STUDY_PROGRESS_OPTIONS } from '@/features/study/interview/const/interview-const'; import { UrlSchema } from '@/shared/util/zod-schema'; -import type { DailyStudyDetail, StudyProgressStatus } from '../api/types'; -import { STUDY_PROGRESS_OPTIONS } from '../consts/study-const'; // 스터디 준비 스키마 export const StudyReadyFormSchema = z.object({ diff --git a/src/features/study/model/use-study-query.ts b/src/features/study/interview/model/use-interview-query.ts similarity index 52% rename from src/features/study/model/use-study-query.ts rename to src/features/study/interview/model/use-interview-query.ts index 8976355c..4a3b32e1 100644 --- a/src/features/study/model/use-study-query.ts +++ b/src/features/study/interview/model/use-interview-query.ts @@ -1,31 +1,13 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { completeStudy, - getDailyStudies, getDailyStudyDetail, - getMonthlyStudyCalendar, - getWeeklyParticipation, - postJoinStudy, putStudyDaily, -} from '@/features/study/api/get-study-data'; +} from '@/features/study/interview/api/get-interview'; import { CompleteStudyRequest, - GetDailyStudiesParams, - GetMonthlyCalendarParams, - JoinStudyRequest, - MonthlyCalendarResponse, PrepareStudyRequest, -} from '../api/types'; - -// 스터디 주간 참여 유무 확인 query -export const useWeeklyParticipation = (params: string) => { - return useQuery({ - queryKey: ['weeklyParticipation', params], - queryFn: () => getWeeklyParticipation(params), - staleTime: 60 * 1000, - enabled: !!params, - }); -}; +} from '@/features/study/interview/api/interview-types'; // 스터디 상세 조회 query export const useDailyStudyDetailQuery = (params: string) => { @@ -37,34 +19,6 @@ export const useDailyStudyDetailQuery = (params: string) => { }); }; -// 스터디 전체 조회 query -export const useDailyStudiesQuery = (params?: GetDailyStudiesParams) => { - return useQuery({ - queryKey: ['dailyStudies', params], - queryFn: () => getDailyStudies(params), - staleTime: 60 * 1000, - }); -}; - -// 스터디 캘린더 조회 query -export const useMonthlyStudyCalendarQuery = ( - params: GetMonthlyCalendarParams, -) => { - return useQuery({ - queryKey: ['monthlyStudyCalendar', params], - queryFn: () => getMonthlyStudyCalendar(params), - staleTime: 60 * 1000, - enabled: !!params?.year && !!params?.month, - }); -}; - -// 스터디 신청 mutation -export const useJoinStudyMutation = () => { - return useMutation({ - mutationFn: (payload: JoinStudyRequest) => postJoinStudy(payload), - }); -}; - // 스터디 상세 & 리스트 업데이트 interface UpdateDailyStudyVariables { dailyStudyId: number; diff --git a/src/features/study/ui/status-badge-map.tsx b/src/features/study/interview/ui/status-badge-map.tsx similarity index 85% rename from src/features/study/ui/status-badge-map.tsx rename to src/features/study/interview/ui/status-badge-map.tsx index 19cbaa63..e786831e 100644 --- a/src/features/study/ui/status-badge-map.tsx +++ b/src/features/study/interview/ui/status-badge-map.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react'; +import { StudyProgressStatus } from '@/features/study/interview/api/interview-types'; import Badge from '@/shared/ui/badge'; -import { StudyProgressStatus } from '../api/types'; export function getStatusBadge(status: StudyProgressStatus): ReactNode { switch (status) { diff --git a/src/features/study/ui/study-done-modal.tsx b/src/features/study/interview/ui/study-done-modal.tsx similarity index 93% rename from src/features/study/ui/study-done-modal.tsx rename to src/features/study/interview/ui/study-done-modal.tsx index b3825ca6..48583647 100644 --- a/src/features/study/ui/study-done-modal.tsx +++ b/src/features/study/interview/ui/study-done-modal.tsx @@ -5,24 +5,23 @@ import { XIcon } from 'lucide-react'; import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import Button from '@/shared/ui/button'; -import { SingleDropdown } from '@/shared/ui/dropdown'; -import FormField from '@/shared/ui/form/form-field'; -import { TextAreaInput } from '@/shared/ui/input'; -import { Modal } from '@/shared/ui/modal'; - import type { CompleteStudyRequest, DailyStudyDetail, StudyProgressStatus, -} from '../api/types'; -import { STUDY_PROGRESS_OPTIONS } from '../consts/study-const'; +} from '@/features/study/interview/api/interview-types'; +import { STUDY_PROGRESS_OPTIONS } from '@/features/study/interview/const/interview-const'; import { StudyDoneFormSchema, type StudyDoneFormValues, buildStudyDoneDefaults, -} from '../model/interview.schema'; -import { useUpdateDailyStudyMutation } from '../model/use-study-query'; +} from '@/features/study/interview/model/interview.schema'; +import { useUpdateDailyStudyMutation } from '@/features/study/interview/model/use-interview-query'; +import Button from '@/shared/ui/button'; +import { SingleDropdown } from '@/shared/ui/dropdown'; +import FormField from '@/shared/ui/form/form-field'; +import { TextAreaInput } from '@/shared/ui/input'; +import { Modal } from '@/shared/ui/modal'; interface StudyDoneModalProps { data: DailyStudyDetail; diff --git a/src/features/study/ui/study-ready-modal.tsx b/src/features/study/interview/ui/study-ready-modal.tsx similarity index 93% rename from src/features/study/ui/study-ready-modal.tsx rename to src/features/study/interview/ui/study-ready-modal.tsx index 56631828..46019b5f 100644 --- a/src/features/study/ui/study-ready-modal.tsx +++ b/src/features/study/interview/ui/study-ready-modal.tsx @@ -5,18 +5,20 @@ import { XIcon } from 'lucide-react'; import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import Button from '@/shared/ui/button'; -import FormField from '@/shared/ui/form/form-field'; -import { BaseInput } from '@/shared/ui/input'; -import { Modal } from '@/shared/ui/modal'; - -import type { DailyStudyDetail, PrepareStudyRequest } from '../api/types'; +import type { + DailyStudyDetail, + PrepareStudyRequest, +} from '@/features/study/interview/api/interview-types'; import { StudyReadyFormSchema, type StudyReadyFormValues, buildStudyReadyDefaults, -} from '../model/interview.schema'; -import { useUpdateDailyStudyMutation } from '../model/use-study-query'; +} from '@/features/study/interview/model/interview.schema'; +import { useUpdateDailyStudyMutation } from '@/features/study/interview/model/use-interview-query'; +import Button from '@/shared/ui/button'; +import FormField from '@/shared/ui/form/form-field'; +import { BaseInput } from '@/shared/ui/input'; +import { Modal } from '@/shared/ui/modal'; interface StudyReadyModalProps { data: DailyStudyDetail; diff --git a/src/features/study/participation/api/get-participation-data.ts b/src/features/study/participation/api/get-participation-data.ts index 82791ff4..ea71e138 100644 --- a/src/features/study/participation/api/get-participation-data.ts +++ b/src/features/study/participation/api/get-participation-data.ts @@ -1,11 +1,13 @@ -import { axiosInstance } from '@/shared/tanstack-query/axios'; import { WeeklyReservationRequest, WeeklyReservationResponse, ReservationUserItem, Participant, -} from './participation-types'; + JoinStudyRequest, +} from '@/features/study/participation/api/participation-types'; +import { axiosInstance } from '@/shared/tanstack-query/axios'; +// 스터디 신청 목록 서버데이터 -> UI 매핑 함수 export function mapReservation(user: ReservationUserItem): Participant { const original = user.profileImage?.resizedImages.find( (img) => img.imageSizeType.imageTypeName === 'ORIGINAL', @@ -19,6 +21,7 @@ export function mapReservation(user: ReservationUserItem): Participant { }; } +// 스터디 신청 목록 export const getReservationMembers = async ( params: WeeklyReservationRequest, ): Promise => { @@ -35,10 +38,18 @@ export const getReservationMembers = async ( return res.data.content; }; -export type StudyStatus = 'RECRUITING' | 'STUDYING'; +// CS 스터디 매칭 신청 +export const postJoinStudy = async (payload: JoinStudyRequest) => { + const cleanPayload = Object.fromEntries( + Object.entries(payload).filter( + ([_, value]) => + value !== undefined && + value !== '' && + !(Array.isArray(value) && value.length === 0), + ), + ); -export const getStudyStatus = async (): Promise => { - const res = await axiosInstance.get('/matching/system-status'); + const res = await axiosInstance.post('/matching/apply', cleanPayload); - return res.data.content.status as StudyStatus; + return res.data; }; diff --git a/src/features/study/participation/api/participation-types.ts b/src/features/study/participation/api/participation-types.ts index 0a5eb343..01fe8178 100644 --- a/src/features/study/participation/api/participation-types.ts +++ b/src/features/study/participation/api/participation-types.ts @@ -1,3 +1,4 @@ +// 다음주 신청 리스트 조회 관련 타입 export interface ReservationUserItem { memberId: number; memberName: string; @@ -37,3 +38,16 @@ export interface WeeklyReservationRequest { pageSize?: number; firstMemberId?: number; } + +// 스터디 참여 신청 타입 +export interface JoinStudyRequest { + memberId: number; + selfIntroduction?: string; + studyPlan?: string; + preferredStudySubjectId?: string; + availableStudyTimeIds?: number[]; + techStackIds?: number[]; + tel?: string; + githubLink?: string; + blogOrSnsLink?: string; +} \ No newline at end of file diff --git a/src/features/study/consts/study-const.ts b/src/features/study/participation/const/participation-const.ts similarity index 87% rename from src/features/study/consts/study-const.ts rename to src/features/study/participation/const/participation-const.ts index 854748db..4171ccf3 100644 --- a/src/features/study/consts/study-const.ts +++ b/src/features/study/participation/const/participation-const.ts @@ -1,10 +1,3 @@ -export const STUDY_PROGRESS_OPTIONS = [ - { label: '시작 전', value: 'PENDING' }, - { label: '불참', value: 'ABSENT' }, - { label: '진행중', value: 'IN_PROGRESS' }, - { label: '완료', value: 'COMPLETE' }, -]; - export const studySteps = [ { title: '1. 면접 준비', diff --git a/src/features/study/participation/model/start-study-form.schema.ts b/src/features/study/participation/model/start-study-form.schema.ts index 5f30714c..a08e6e93 100644 --- a/src/features/study/participation/model/start-study-form.schema.ts +++ b/src/features/study/participation/model/start-study-form.schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; +import { JoinStudyRequest } from '@/features/study/participation/api/participation-types'; import { UrlSchema } from '@/shared/util/zod-schema'; -import { JoinStudyRequest } from '../../api/types'; export const StartStudyFormSchema = z.object({ selfIntroduction: z diff --git a/src/features/study/participation/model/use-participation-query.ts b/src/features/study/participation/model/use-participation-query.ts index c4d46719..b976e555 100644 --- a/src/features/study/participation/model/use-participation-query.ts +++ b/src/features/study/participation/model/use-participation-query.ts @@ -1,12 +1,22 @@ -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useMutation } from '@tanstack/react-query'; import { getReservationMembers, - getStudyStatus, mapReservation, - StudyStatus, -} from '../api/get-participation-data'; -import { WeeklyReservationResponse } from '../api/participation-types'; + postJoinStudy, +} from '@/features/study/participation/api/get-participation-data'; +import { + JoinStudyRequest, + WeeklyReservationResponse, +} from '@/features/study/participation/api/participation-types'; + +// 스터디 신청 mutation +export const useJoinStudyMutation = () => { + return useMutation({ + mutationFn: (payload: JoinStudyRequest) => postJoinStudy(payload), + }); +}; +// 다음주차 스터디 신청 무한 스크롤 export function useInfiniteReservation(firstMemberId?: number, pageSize = 50) { return useInfiniteQuery({ queryKey: ['weeklyReservationMembers', { firstMemberId, pageSize }], @@ -44,11 +54,3 @@ export function useInfiniteReservation(firstMemberId?: number, pageSize = 50) { staleTime: 60 * 1000, }); } - -export const useStudyStatusQuery = () => { - return useQuery({ - queryKey: ['studyStatus'], - queryFn: getStudyStatus, - staleTime: 60 * 1000, - }); -}; diff --git a/src/features/study/participation/ui/reservation-list.tsx b/src/features/study/participation/ui/reservation-list.tsx index 912afdef..92c6fb13 100644 --- a/src/features/study/participation/ui/reservation-list.tsx +++ b/src/features/study/participation/ui/reservation-list.tsx @@ -7,9 +7,9 @@ import { useUserProfileQuery, } from '@/entities/user/model/use-user-profile-query'; import ProfileDefault from '@/entities/user/ui/icon/profile-default.svg'; +import ReservationCard from '@/features/study/participation/ui/reservation-user-card'; +import StartStudyModal from '@/features/study/participation/ui/start-study-modal'; import { getCookie } from '@/shared/tanstack-query/cookie'; -import ReservationCard from './reservation-user-card'; -import StartStudyModal from '../../ui/start-study-modal'; import { useInfiniteReservation } from '../model/use-participation-query'; interface ReservationListProps { diff --git a/src/features/study/ui/start-study-modal.tsx b/src/features/study/participation/ui/start-study-modal.tsx similarity index 97% rename from src/features/study/ui/start-study-modal.tsx rename to src/features/study/participation/ui/start-study-modal.tsx index 1330e84d..af53ff0d 100644 --- a/src/features/study/ui/start-study-modal.tsx +++ b/src/features/study/participation/ui/start-study-modal.tsx @@ -12,6 +12,14 @@ import { useTechStacksQuery, } from '@/features/my-page/model/use-update-user-profile-mutation'; +import { studySteps } from '@/features/study/participation/const/participation-const'; +import { + StartStudyFormSchema, + type StartStudyFormValues, + buildStartStudyDefaultValues, + toJoinStudyRequest, +} from '@/features/study/participation/model/start-study-form.schema'; +import { useJoinStudyMutation } from '@/features/study/participation/model/use-participation-query'; import Button from '@/shared/ui/button'; import { SingleDropdown, MultiDropdown } from '@/shared/ui/dropdown'; import FormField from '@/shared/ui/form/form-field'; @@ -19,15 +27,6 @@ import { BaseInput, TextAreaInput } from '@/shared/ui/input'; import { Modal } from '@/shared/ui/modal'; import { ToggleGroup } from '@/shared/ui/toggle'; -import { studySteps } from '../consts/study-const'; - -import { useJoinStudyMutation } from '../model/use-study-query'; -import { - StartStudyFormSchema, - type StartStudyFormValues, - buildStartStudyDefaultValues, - toJoinStudyRequest, -} from '../participation/model/start-study-form.schema'; interface StartStudyModalProps { memberId: number; diff --git a/src/features/study/schedule/api/get-study-schedule.tsx b/src/features/study/schedule/api/get-study-schedule.tsx new file mode 100644 index 00000000..b2345f0d --- /dev/null +++ b/src/features/study/schedule/api/get-study-schedule.tsx @@ -0,0 +1,46 @@ +// 매칭 결과 목록, 오늘의 스터디 상세 정보, 나의 스터디 캘린더 +import { + GetDailyStudiesParams, + GetDailyStudiesResponse, + GetMonthlyCalendarParams, + MonthlyCalendarResponse, + StudyStatus, + WeeklyParticipationResponse, +} from '@/features/study/schedule/api/schedule-types'; +import { axiosInstance } from '@/shared/tanstack-query/axios'; + +// 스터디 전체 조회 +export const getDailyStudies = async ( + params?: GetDailyStudiesParams, +): Promise => { + const res = await axiosInstance.get('/study/daily', { params }); + + return res.data.content; +}; + +// 월 별 스터디 캘린더 조회 +export const getMonthlyStudyCalendar = async ( + params: GetMonthlyCalendarParams, +): Promise => { + const res = await axiosInstance.get('/study/daily/month', { params }); + + return res.data.content; +}; + +// 스터디 참여 유무 확인 +export const getWeeklyParticipation = async ( + studyDate: string, +): Promise => { + const res = await axiosInstance.get('/study/week/participation', { + params: { studyDate }, + }); + + return res.data.content; +}; + +// 스터디 시작/종료 유무 확인 +export const getStudyStatus = async (): Promise => { + const res = await axiosInstance.get('/matching/system-status'); + + return res.data.content.status as StudyStatus; +}; diff --git a/src/features/study/schedule/api/schedule-types.ts b/src/features/study/schedule/api/schedule-types.ts new file mode 100644 index 00000000..fbc248ef --- /dev/null +++ b/src/features/study/schedule/api/schedule-types.ts @@ -0,0 +1,58 @@ +export type StudyProgressStatus = + | 'PENDING' + | 'IN_PROGRESS' + | 'COMPLETE' + | 'ABSENT'; + +// 스터디 시작/종료 유무 타입 +export type StudyStatus = 'RECRUITING' | 'STUDYING'; + +// 스터디 매칭 리스트 관련 타입 +export interface GetDailyStudiesParams { + cursor?: number; + pageSize?: number; + studyDate?: string; +} + +export interface DailyStudy { + interviewer: string; + interviewerImage: string; + interviewee: string; + intervieweeImage: string; + dailyStudyId: number; + subject: string; + description: string; + link: string; + progressStatus: StudyProgressStatus; + studyDate: string; + feedback: string | undefined; +} + +export interface GetDailyStudiesResponse { + items: DailyStudy[]; + nextCursor: number; + hasNext: boolean; +} + +// 캘린더 관련 타입 +export interface GetMonthlyCalendarParams { + year: number; + month: number; +} + +export interface StudyCalendarDay { + day: number; + hasStudy: boolean; + status: StudyProgressStatus | undefined; +} + +export interface MonthlyCalendarResponse { + calendar: StudyCalendarDay[]; + monthlyCompletedCount?: number; + totalCompletedCount?: number; +} + +export interface WeeklyParticipationResponse { + memberId: number; + isParticipate: boolean; +} diff --git a/src/features/study/schedule/model/use-schedule-query.ts b/src/features/study/schedule/model/use-schedule-query.ts new file mode 100644 index 00000000..68c9d28a --- /dev/null +++ b/src/features/study/schedule/model/use-schedule-query.ts @@ -0,0 +1,52 @@ +import { useQuery } from '@tanstack/react-query'; +import { + getDailyStudies, + getMonthlyStudyCalendar, + getStudyStatus, + getWeeklyParticipation, +} from '@/features/study/schedule/api/get-study-schedule'; +import { + GetDailyStudiesParams, + GetMonthlyCalendarParams, + MonthlyCalendarResponse, + StudyStatus, +} from '@/features/study/schedule/api/schedule-types'; + +// 스터디 주간 참여 유무 확인 query +export const useWeeklyParticipation = (params: string) => { + return useQuery({ + queryKey: ['weeklyParticipation', params], + queryFn: () => getWeeklyParticipation(params), + staleTime: 60 * 1000, + enabled: !!params, + }); +}; + +// 스터디 매칭 결과 조회 query +export const useDailyStudiesQuery = (params?: GetDailyStudiesParams) => { + return useQuery({ + queryKey: ['dailyStudies', params], + queryFn: () => getDailyStudies(params), + staleTime: 60 * 1000, + }); +}; + +// 스터디 캘린더 조회 query +export const useMonthlyStudyCalendarQuery = ( + params: GetMonthlyCalendarParams, +) => { + return useQuery({ + queryKey: ['monthlyStudyCalendar', params], + queryFn: () => getMonthlyStudyCalendar(params), + staleTime: 60 * 1000, + enabled: !!params?.year && !!params?.month, + }); +}; + +export const useStudyStatusQuery = () => { + return useQuery({ + queryKey: ['studyStatus'], + queryFn: getStudyStatus, + staleTime: 60 * 1000, + }); +}; diff --git a/src/features/study/ui/data-selector.tsx b/src/features/study/schedule/ui/data-selector.tsx similarity index 100% rename from src/features/study/ui/data-selector.tsx rename to src/features/study/schedule/ui/data-selector.tsx diff --git a/src/features/study/ui/study-card.tsx b/src/features/study/schedule/ui/study-card.tsx similarity index 87% rename from src/features/study/ui/study-card.tsx rename to src/features/study/schedule/ui/study-card.tsx index 453ccd4c..50f27367 100644 --- a/src/features/study/ui/study-card.tsx +++ b/src/features/study/schedule/ui/study-card.tsx @@ -2,17 +2,19 @@ import { getMonth, getDay, startOfWeek, getDate } from 'date-fns'; import { useMemo, useState } from 'react'; +import ReservationList from '@/features/study/participation/ui/reservation-list'; +import { + useStudyStatusQuery, + useWeeklyParticipation, +} from '@/features/study/schedule/model/use-schedule-query'; +import DateSelector from '@/features/study/schedule/ui/data-selector'; +import TodayStudyCard from '@/features/study/schedule/ui/today-study-card'; import { formatKoreaYMD, getKoreaDate, getKoreaDisplayMonday, } from '@/shared/lib/time'; -import DateSelector from './data-selector'; -import TodayStudyCard from './today-study-card'; -import StudyListSection from '../../../widgets/home/study-list-table'; -import { useWeeklyParticipation } from '../model/use-study-query'; -import { useStudyStatusQuery } from '../participation/model/use-participation-query'; -import ReservationList from '../participation/ui/reservation-list'; +import StudyListSection from '../../../../widgets/home/study-list-table'; // 스터디 주차 구하는 함수 function getWeekly(date: Date): { month: number; week: number } { diff --git a/src/features/study/ui/today-study-card.tsx b/src/features/study/schedule/ui/today-study-card.tsx similarity index 89% rename from src/features/study/ui/today-study-card.tsx rename to src/features/study/schedule/ui/today-study-card.tsx index a1816bb4..9bb89839 100644 --- a/src/features/study/ui/today-study-card.tsx +++ b/src/features/study/schedule/ui/today-study-card.tsx @@ -2,14 +2,12 @@ import { useEffect, useState } from 'react'; import UserProfileModal from '@/entities/user/ui/user-profile-modal'; -import { getStatusBadge } from '@/features/study/ui/status-badge-map'; +import { useDailyStudyDetailQuery } from '@/features/study/interview/model/use-interview-query'; +import { getStatusBadge } from '@/features/study/interview/ui/status-badge-map'; +import StudyDoneModal from '@/features/study/interview/ui/study-done-modal'; +import StudyReadyModal from '@/features/study/interview/ui/study-ready-modal'; import { getCookie } from '@/shared/tanstack-query/cookie'; -// TODO: FSD 의 import 바운더리를 넘어서 import 해야하는데, -// 해당 UI를 shared 등으로 빼던지 수정 필요 import UserAvatar from '@/shared/ui/avatar'; -import StudyDoneModal from './study-done-modal'; -import StudyReadyModal from './study-ready-modal'; -import { useDailyStudyDetailQuery } from '../model/use-study-query'; export default function TodayStudyCard({ studyDate }: { studyDate: string }) { const [memberId, setMemberId] = useState(null); diff --git a/src/widgets/home/calendar.tsx b/src/widgets/home/calendar.tsx index 402fd09d..9a0ba2a8 100644 --- a/src/widgets/home/calendar.tsx +++ b/src/widgets/home/calendar.tsx @@ -7,7 +7,7 @@ import { type CalendarDay as DayPickerDay, type Modifiers, } from 'react-day-picker'; -import { useMonthlyStudyCalendarQuery } from '@/features/study/model/use-study-query'; +import { useMonthlyStudyCalendarQuery } from '@/features/study/schedule/model/use-schedule-query'; import { cn } from '@/shared/shadcn/lib/utils'; import { Calendar as ShadcnCalendar } from '@/shared/shadcn/ui/calendar'; diff --git a/src/widgets/home/sidebar.tsx b/src/widgets/home/sidebar.tsx index 667e7bc0..2c95571a 100644 --- a/src/widgets/home/sidebar.tsx +++ b/src/widgets/home/sidebar.tsx @@ -2,7 +2,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { getUserProfileInServer } from '@/entities/user/api/get-user-profile.server'; import MyProfileCard from '@/entities/user/ui/my-profile-card'; -import StartStudyModal from '@/features/study/ui/start-study-modal'; +import StartStudyModal from '@/features/study/participation/ui/start-study-modal'; import { getServerCookie } from '@/shared/lib/server-cookie'; import Calendar from '@/widgets/home/calendar'; import TodoList from '@/widgets/home/todo-list'; diff --git a/src/widgets/home/study-list-table.tsx b/src/widgets/home/study-list-table.tsx index 12ec3d98..2e502322 100644 --- a/src/widgets/home/study-list-table.tsx +++ b/src/widgets/home/study-list-table.tsx @@ -1,9 +1,9 @@ -import { useDailyStudiesQuery } from '@/features/study/model/use-study-query'; -import { getStatusBadge } from '@/features/study/ui/status-badge-map'; +import { getStatusBadge } from '@/features/study/interview/ui/status-badge-map'; +import { DailyStudy } from '@/features/study/schedule/api/schedule-types'; +import { useDailyStudiesQuery } from '@/features/study/schedule/model/use-schedule-query'; import UserAvatar from '@/shared/ui/avatar'; import TableList from '@/shared/ui/table'; import LinkIcon from 'public/icons/Link.svg'; -import { DailyStudy } from '../../features/study/api/types'; const headers = [ '조',