-
+
+
{profile.memberProfile.githubLink?.url ??
'깃허브 링크를 입력해주세요!'}
-
-
+
+
{profile.memberProfile.blogOrSnsLink?.url ??
'블로그 링크를 입력해주세요!'}
diff --git a/src/features/study/api/get-study-data.ts b/src/features/study/api/get-study-data.ts
index 07cc4932..570e7b4e 100644
--- a/src/features/study/api/get-study-data.ts
+++ b/src/features/study/api/get-study-data.ts
@@ -1,27 +1,23 @@
import type {
+ CompleteStudyRequest,
DailyStudyDetail,
GetDailyStudiesParams,
GetDailyStudiesResponse,
- GetDailyStudyDetailParams,
GetDailyStudyDetailParams2,
GetMonthlyCalendarParams,
JoinStudyRequest,
MonthlyCalendarResponse,
PostDailyRetrospectRequest,
- PutRetrospectRequest,
- PutStudyDailyRequest,
- StudyProgressStatus,
+ PrepareStudyRequest,
WeeklyParticipationResponse,
} from '@/features/study/api/types';
import { axiosInstance } from '@/shared/tanstack-query/axios';
// 스터디 상세 조회
export const getDailyStudyDetail = async (
- params: GetDailyStudyDetailParams,
+ params: string,
): Promise
=> {
- const { studyDate } = params;
-
- const res = await axiosInstance.get(`/study/daily/mine/${studyDate}`);
+ const res = await axiosInstance.get(`/study/daily/mine/${params}`);
return res.data.content;
};
@@ -50,39 +46,23 @@ export const postDailyRetrospect = async (body: PostDailyRetrospectRequest) => {
return res.data;
};
-// 피면접자 스터디 업데이트
+// 면접 준비 시작
export const putStudyDaily = async (
dailyId: number,
- body: PutStudyDailyRequest,
+ body: PrepareStudyRequest,
) => {
- const res = await axiosInstance.put(`/study/daily/${dailyId}`, body);
+ const res = await axiosInstance.put(`/study/daily/${dailyId}/prepare`, body);
return res.data;
};
-// 면접자 스터디 업데이트 [스터디 진행 상태]
-export const patchStudyStatus = async (
+// 면접 완료 및 회고 작성
+export const completeStudy = async (
dailyStudyId: number,
- progressStatus: StudyProgressStatus,
-) => {
- const res = await axiosInstance.patch(
- `/study/daily/${dailyStudyId}/status`,
- null,
- {
- params: { progressStatus },
- },
- );
-
- return res.data;
-};
-
-// 면접자 스터디 업데이트 [피드백]
-export const putRetrospect = async (
- retrospectId: number,
- body: PutRetrospectRequest,
+ body: CompleteStudyRequest,
) => {
- const res = await axiosInstance.put(
- `/study/daily/retrospect/${retrospectId}`,
+ const res = await axiosInstance.post(
+ `/study/daily/${dailyStudyId}/complete`,
body,
);
diff --git a/src/features/study/api/types.ts b/src/features/study/api/types.ts
index 978f3503..7dbf0b62 100644
--- a/src/features/study/api/types.ts
+++ b/src/features/study/api/types.ts
@@ -34,10 +34,6 @@ export interface DailyStudyDetail {
feedback: string;
}
-export interface GetDailyStudyDetailParams {
- studyDate: string;
-}
-
export interface GetDailyStudyDetailParams2 {
year: number;
month: number;
@@ -47,9 +43,7 @@ export interface GetDailyStudyDetailParams2 {
export interface GetDailyStudiesParams {
cursor?: number;
pageSize?: number;
- year?: number;
- month?: number;
- day?: number;
+ studyDate?: string;
}
export interface GetDailyStudiesResponse {
@@ -80,9 +74,8 @@ export interface PostDailyRetrospectRequest {
parentId: number;
}
-export interface PutStudyDailyRequest {
+export interface PrepareStudyRequest {
subject: string;
- description: string;
link: string;
}
@@ -103,6 +96,7 @@ export interface WeeklyParticipationResponse {
isParticipate: boolean;
}
-export interface PutRetrospectRequest {
- description: string;
+export interface CompleteStudyRequest {
+ feedback: string;
+ progressStatus: StudyProgressStatus;
}
diff --git a/src/features/study/model/use-study-query.ts b/src/features/study/model/use-study-query.ts
index 091764ca..72afc479 100644
--- a/src/features/study/model/use-study-query.ts
+++ b/src/features/study/model/use-study-query.ts
@@ -1,20 +1,24 @@
-import { useMutation, useQuery } from '@tanstack/react-query';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
+ completeStudy,
getDailyStudies,
getDailyStudyDetail,
getMonthlyStudyCalendar,
getWeeklyParticipation,
postJoinStudy,
+ putStudyDaily,
} from '@/features/study/api/get-study-data';
import {
+ CompleteStudyRequest,
GetDailyStudiesParams,
- GetDailyStudyDetailParams,
GetDailyStudyDetailParams2,
GetMonthlyCalendarParams,
JoinStudyRequest,
MonthlyCalendarResponse,
+ PrepareStudyRequest,
} from '../api/types';
+// 스터디 주간 참여 유무 확인 query
export const useWeeklyParticipation = (params: GetDailyStudyDetailParams2) => {
return useQuery({
queryKey: ['weeklyParticipation', params],
@@ -23,18 +27,17 @@ export const useWeeklyParticipation = (params: GetDailyStudyDetailParams2) => {
});
};
-export const useDailyStudyDetailQuery = (
- params: GetDailyStudyDetailParams,
- enabled: boolean = true,
-) => {
+// 스터디 상세 조회 query
+export const useDailyStudyDetailQuery = (params: string) => {
return useQuery({
queryKey: ['dailyStudyDetail', params],
queryFn: () => getDailyStudyDetail(params),
staleTime: 60 * 1000,
- enabled: enabled && !!params,
+ enabled: !!params,
});
};
+// 스터디 전체 조회 query
export const useDailyStudiesQuery = (params?: GetDailyStudiesParams) => {
return useQuery({
queryKey: ['dailyStudies', params],
@@ -43,6 +46,7 @@ export const useDailyStudiesQuery = (params?: GetDailyStudiesParams) => {
});
};
+// 스터디 캘린더 조회 query
export const useMonthlyStudyCalendarQuery = (
params: GetMonthlyCalendarParams,
) => {
@@ -54,8 +58,42 @@ export const useMonthlyStudyCalendarQuery = (
});
};
+// 스터디 신청 mutation
export const useJoinStudyMutation = () => {
return useMutation({
mutationFn: (payload: JoinStudyRequest) => postJoinStudy(payload),
});
};
+
+// 스터디 상세 & 리스트 업데이트
+interface UpdateDailyStudyVariables {
+ dailyStudyId: number;
+ studyDate: string;
+ form: PrepareStudyRequest | CompleteStudyRequest;
+ requestType: 'prepare' | 'complete';
+}
+
+export const useUpdateDailyStudyMutation = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ dailyStudyId, form, requestType }) => {
+ if (requestType === 'prepare') {
+ await putStudyDaily(dailyStudyId, form as PrepareStudyRequest);
+ } else {
+ await completeStudy(dailyStudyId, form as CompleteStudyRequest);
+ }
+ },
+ onSuccess: async (_data, { studyDate }) => {
+ await queryClient.invalidateQueries({
+ queryKey: ['dailyStudyDetail', studyDate],
+ exact: true,
+ });
+
+ await queryClient.invalidateQueries({
+ queryKey: ['dailyStudies', { studyDate }],
+ exact: false,
+ });
+ },
+ });
+};
diff --git a/src/features/study/ui/start-study-modal.tsx b/src/features/study/ui/start-study-modal.tsx
index bbf66317..e411b252 100644
--- a/src/features/study/ui/start-study-modal.tsx
+++ b/src/features/study/ui/start-study-modal.tsx
@@ -321,6 +321,7 @@ export default function StartStudyModal({ memberId }: StartStudyModalProps) {
},
{
onSuccess: () => {
+ alert('스터디 신청이 완료되었습니다!');
router.refresh();
},
onError: () => {
diff --git a/src/features/study/ui/status-badge-map.tsx b/src/features/study/ui/status-badge-map.tsx
new file mode 100644
index 00000000..19cbaa63
--- /dev/null
+++ b/src/features/study/ui/status-badge-map.tsx
@@ -0,0 +1,18 @@
+import type { ReactNode } from 'react';
+import Badge from '@/shared/ui/badge';
+import { StudyProgressStatus } from '../api/types';
+
+export function getStatusBadge(status: StudyProgressStatus): ReactNode {
+ switch (status) {
+ case 'PENDING':
+ return 시작 전;
+ case 'IN_PROGRESS':
+ return 진행중;
+ case 'COMPLETE':
+ return 완료;
+ case 'ABSENT':
+ return 불참;
+ default:
+ return null;
+ }
+}
diff --git a/src/features/study/ui/study-card.tsx b/src/features/study/ui/study-card.tsx
index 4ce50232..6cb4faa4 100644
--- a/src/features/study/ui/study-card.tsx
+++ b/src/features/study/ui/study-card.tsx
@@ -5,10 +5,7 @@ import { useState } from 'react';
import DateSelector from './data-selector';
import TodayStudyCard from './today-study-card';
import StudyListSection from '../../../widgets/home/study-list-table';
-import {
- useDailyStudyDetailQuery,
- useWeeklyParticipation,
-} from '../model/use-study-query';
+import { useWeeklyParticipation } from '../model/use-study-query';
// 스터디 주차 구하는 함수
function getWeekly(date: Date): { month: number; week: number } {
@@ -62,15 +59,14 @@ export default function StudyCard() {
day: selectedDate.getDate(),
};
- const studyParams = { studyDate: selectedDate.toISOString().split('T')[0] };
+ const offset = selectedDate.getTimezoneOffset() * 60000; // ms단위라 60000곱해줌
+ const dateOffset = new Date(selectedDate.getTime() - offset);
+
+ const studyDate = dateOffset.toISOString().split('T')[0];
const { data: participationData } = useWeeklyParticipation(params);
const isParticipate = participationData?.isParticipate ?? false;
- const { data: todayStudyData, refetch } = useDailyStudyDetailQuery(
- studyParams,
- isParticipate,
- );
const { month, week } = getWeekly(selectedDate);
return (
@@ -80,10 +76,8 @@ export default function StudyCard() {
- {isParticipate && todayStudyData && (
-
- )}
-
+ {isParticipate && }
+
>
);
diff --git a/src/features/study/ui/study-done-modal.tsx b/src/features/study/ui/study-done-modal.tsx
new file mode 100644
index 00000000..6224f2d3
--- /dev/null
+++ b/src/features/study/ui/study-done-modal.tsx
@@ -0,0 +1,163 @@
+'use client';
+
+import { XIcon } from 'lucide-react';
+import { useState } from 'react';
+import Button from '@/shared/ui/button';
+import { SingleDropdown } from '@/shared/ui/dropdown';
+import { TextAreaInput } from '@/shared/ui/input';
+import { Modal } from '@/shared/ui/modal';
+import {
+ CompleteStudyRequest,
+ DailyStudyDetail,
+ StudyProgressStatus,
+} from '../api/types';
+import { STUDY_PROGRESS_OPTIONS } from '../consts/study-const';
+import { useUpdateDailyStudyMutation } from '../model/use-study-query';
+
+interface StudyDoneModalProps {
+ data: DailyStudyDetail;
+ studyDate: string;
+}
+
+export default function StudyDoneModal({
+ data,
+ studyDate,
+}: StudyDoneModalProps) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+
+
+
+
+
+
+ 면접 완료하기
+
+
+
+
+
+ setIsOpen(false)}
+ />
+
+
+
+ );
+}
+
+interface StudyDoneFormProps {
+ data: DailyStudyDetail;
+ studyDate: string;
+ onClose: () => void;
+}
+
+function StudyDoneForm({ data, studyDate, onClose }: StudyDoneFormProps) {
+ const [form, setForm] = useState
({
+ feedback: data.feedback ?? '',
+ progressStatus: data.progressStatus ?? 'PENDING',
+ });
+
+ const { mutate, isPending } = useUpdateDailyStudyMutation();
+ const { feedback, progressStatus } = form;
+
+ const handleChange = (key: keyof CompleteStudyRequest) => (value: string) => {
+ setForm((prev) => ({ ...prev, [key]: value }));
+ };
+
+ const handleSubmit = async () => {
+ if (!feedback.trim() || !progressStatus) return;
+
+ mutate(
+ {
+ dailyStudyId: data.dailyStudyId,
+ studyDate,
+ form,
+ requestType: 'complete',
+ },
+ {
+ onSuccess: onClose,
+ onError: (err) => {
+ console.error(err);
+ alert('요청 처리에 실패했습니다. 다시 시도해주세요.');
+ },
+ },
+ );
+ };
+
+ return (
+ <>
+
+
+
+
+
+ 면접 완료 후 해당 지원자의 상태를 업데이트해 주세요.
+
+
+
+
+ handleChange('progressStatus')(value as StudyProgressStatus)
+ }
+ />
+
+
+
+
+
+
+ 면접 결과에 대한 간단한 피드백을 입력해 주세요.
+
+
+
+
handleChange('feedback')(value)}
+ />
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/features/study/ui/study-ready-modal.tsx b/src/features/study/ui/study-ready-modal.tsx
new file mode 100644
index 00000000..2926eb22
--- /dev/null
+++ b/src/features/study/ui/study-ready-modal.tsx
@@ -0,0 +1,146 @@
+'use client';
+
+import { XIcon } from 'lucide-react';
+import { useState } from 'react';
+import Button from '@/shared/ui/button';
+import { BaseInput } from '@/shared/ui/input';
+import { Modal } from '@/shared/ui/modal';
+import { DailyStudyDetail, PrepareStudyRequest } from '../api/types';
+import { useUpdateDailyStudyMutation } from '../model/use-study-query';
+
+interface StudyReadyModalProps {
+ data: DailyStudyDetail;
+ studyDate: string;
+}
+
+export default function StudyReadyModal({
+ data,
+ studyDate,
+}: StudyReadyModalProps) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+
+
+
+
+
+
+
+
+ 면접 준비하기
+
+
+
+
+
+ setIsOpen(false)}
+ />
+
+
+
+ );
+}
+
+interface StudyReadyFormProps {
+ data: DailyStudyDetail;
+ studyDate: string;
+ onClose: () => void;
+}
+
+function StudyReadyForm({ data, studyDate, onClose }: StudyReadyFormProps) {
+ const [form, setForm] = useState({
+ subject: data.subject ?? '',
+ link: data.link ?? '',
+ });
+
+ const { mutate, isPending } = useUpdateDailyStudyMutation();
+ const { subject, link } = form;
+
+ const handleChange = (key: keyof PrepareStudyRequest) => (value: string) => {
+ setForm((prev) => ({ ...prev, [key]: value }));
+ };
+
+ const handleSubmit = async () => {
+ if (!subject.trim()) return;
+
+ mutate(
+ {
+ dailyStudyId: data.dailyStudyId,
+ studyDate,
+ form,
+ requestType: 'prepare',
+ },
+ {
+ onSuccess: onClose,
+ onError: (err) => {
+ console.error(err);
+ alert('요청 처리에 실패했습니다. 다시 시도해주세요.');
+ },
+ },
+ );
+ };
+
+ return (
+ <>
+
+
+
+
+
+ 이번 스터디에서 다룰 면접 주제를 입력하세요
+
+
+
+
handleChange('subject')(e.target.value)}
+ />
+
+
+
+
+
+
+ 참고할 링크나 자료가 있다면 입력해 주세요
+
+
+
+
handleChange('link')(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/features/study/ui/today-study-card.tsx b/src/features/study/ui/today-study-card.tsx
index 6ad64340..109f265e 100644
--- a/src/features/study/ui/today-study-card.tsx
+++ b/src/features/study/ui/today-study-card.tsx
@@ -1,51 +1,65 @@
'use client';
+import { useEffect, useState } from 'react';
+import { getStatusBadge } from '@/features/study/ui/status-badge-map';
+import { getCookie } from '@/shared/tanstack-query/cookie';
import UserAvatar from '@/shared/ui/avatar';
-import Badge from '@/shared/ui/badge';
-import TodayStudyModal from './today-study-modal';
-import { DailyStudyDetail, StudyProgressStatus } from '../api/types';
+import StudyDoneModal from './study-done-modal';
+import StudyReadyModal from './study-ready-modal';
+import { useDailyStudyDetailQuery } from '../model/use-study-query';
-interface Props {
- data: DailyStudyDetail;
- refetch: () => void;
-}
+export default function TodayStudyCard({ studyDate }: { studyDate: string }) {
+ const [memberId, setMemberId] = useState(null);
+
+ useEffect(() => {
+ const id = getCookie('memberId');
+ setMemberId(id ? Number(id) : null);
+ }, []);
+
+ const { data: todayStudyData } = useDailyStudyDetailQuery(studyDate);
+
+ if (!todayStudyData) return null;
-const statusBadgeMap: Partial> = {
- PENDING: 시작 전,
- IN_PROGRESS: 진행중,
- COMPLETE: 완료,
- ABSENT: 불참,
-};
+ const isInterviewee = memberId === todayStudyData.intervieweeId;
-export default function TodayStudyCard({ data, refetch }: Props) {
return (
오늘의 스터디
-
+ {memberId !== null &&
+ (isInterviewee ? (
+
+ ) : (
+
+ ))}
-
+
-
- {data.interviewerName}
+
+
+ {todayStudyData.interviewerName}
+
}
/>
-
+
);
@@ -64,8 +78,4 @@ export default function TodayStudyCard({ data, refetch }: Props) {