From 4753910c32766278815230cda66fa65f13f63dd2 Mon Sep 17 00:00:00 2001 From: aken-you Date: Thu, 9 Oct 2025 21:23:32 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A3=B9=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=EC=8B=A0=EC=B2=AD=20=EB=AA=A8=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/ui/apply-group-study-modal.tsx | 186 ++++++++++++++++++ src/widgets/home/sidebar.tsx | 6 + 2 files changed, 192 insertions(+) create mode 100644 src/features/study/group/ui/apply-group-study-modal.tsx diff --git a/src/features/study/group/ui/apply-group-study-modal.tsx b/src/features/study/group/ui/apply-group-study-modal.tsx new file mode 100644 index 00000000..44c9f106 --- /dev/null +++ b/src/features/study/group/ui/apply-group-study-modal.tsx @@ -0,0 +1,186 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { XIcon } from 'lucide-react'; +import { useState } from 'react'; + +import { FormProvider, useForm } from 'react-hook-form'; +import { z } from 'zod'; +import Button from '@/shared/ui/button'; +import Checkbox from '@/shared/ui/checkbox'; +import { TextAreaInput } from '@/shared/ui/input'; +import { Modal } from '@/shared/ui/modal'; + +interface ApplyGroupStudyModalProps { + title: string; + questions: string[]; +} + +export default function ApplyGroupStudyModal({ + title, + questions, +}: ApplyGroupStudyModalProps) { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + + + 스터디 신청서 작성하기 + + setOpen(false)}> + + + + + setOpen(false)} + /> + + + + ); +} + +const ApplyGroupStudyFormSchema = z.object({ + answer: z.array(z.string().min(1, '답변을 작성해주세요.')), +}); + +type ApplyGroupStudyFormValues = z.infer; + +function buildApplyGroupStudyDefaultValues(): ApplyGroupStudyFormValues { + return { + answer: [], + }; +} + +function ApplyGroupStudyForm({ + title, + questions, + onClose, +}: { + title: string; + questions: string[]; + onClose: () => void; +}) { + const [checked, setChecked] = useState(false); + const methods = useForm<{ answer: string[] }>({ + resolver: zodResolver(ApplyGroupStudyFormSchema), + mode: 'onChange', + defaultValues: buildApplyGroupStudyDefaultValues(), + }); + + const handleApply = () => {}; + + return ( + <> + +

{title}

+ +
+ + 리더의 질문 + + + +
+ {questions.map((question, index) => ( +
+ + +
+ ))} +
+
+
+ +
+

스터디 참여 규칙을 준수해주세요

+ +
    +
  • + · + 모집 공고의 정보는 사실에 기반해 작성해야 합니다. +
  • +
  • + · + 진행 기간과 모임 빈도를 명확히 안내해야 합니다. +
  • +
  • + · + + 스터디 개설 후 최소 1회 이상 정기 모임을 진행해야 합니다. + +
  • +
  • + · + 스터디원과의 약속을 존중하고 성실히 운영해야 합니다. +
  • +
  • + · + + 타인의 개인정보 및 자료를 무단으로 공유하거나 외부 유출해서는 안 + 됩니다. + +
  • +
  • + · + + 플랫폼의 운영 정책 및 커뮤니티 가이드를 준수해야 합니다. + +
  • +
+ +
+
+ + 참여 규칙을 확인하시고 동의해 주시겠습니까? + + 필수 +
+ + { + setChecked((prev) => !prev); + }} + /> +
+
+
+ + + + + + + + + ); +} diff --git a/src/widgets/home/sidebar.tsx b/src/widgets/home/sidebar.tsx index 2c95571a..b4a10030 100644 --- a/src/widgets/home/sidebar.tsx +++ b/src/widgets/home/sidebar.tsx @@ -2,6 +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 ApplyGroupStudyModal from '@/features/study/group/ui/apply-group-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'; @@ -74,6 +75,11 @@ export default async function Sidebar() { 피드백 + + ); From 2df707fb14511d29c36ee7e9b6bb3f33c6cccb53 Mon Sep 17 00:00:00 2001 From: aken-you Date: Fri, 10 Oct 2025 22:25:14 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A3=B9=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=EC=8B=A0=EC=B2=AD=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/auth/ui/landing.tsx | 11 ++ .../study/group/api/apply-group-study.ts | 17 +++ .../study/group/api/group-study-types.ts | 17 +++ .../group/model/use-apply-group-study.tsx | 9 ++ .../group/ui/apply-group-study-modal.tsx | 118 ++++++++++++------ src/widgets/home/sidebar.tsx | 4 - 6 files changed, 135 insertions(+), 41 deletions(-) create mode 100644 src/features/study/group/api/apply-group-study.ts create mode 100644 src/features/study/group/api/group-study-types.ts create mode 100644 src/features/study/group/model/use-apply-group-study.tsx diff --git a/src/features/auth/ui/landing.tsx b/src/features/auth/ui/landing.tsx index df785b52..50c82083 100644 --- a/src/features/auth/ui/landing.tsx +++ b/src/features/auth/ui/landing.tsx @@ -4,6 +4,7 @@ import Image from 'next/image'; import { useState, useEffect } from 'react'; import LoginModal from '@/features/auth/ui/login-modal'; import SignupModal from '@/features/auth/ui/sign-up-modal'; +import ApplyGroupStudyModal from '@/features/study/group/ui/apply-group-study-modal'; import Button from '@/shared/ui/button'; export default function Landing({ isSignupPage }: { isSignupPage: boolean }) { @@ -36,6 +37,16 @@ export default function Landing({ isSignupPage }: { isSignupPage: boolean }) { /> setSignupOpen(false)} /> + {/* 테스트용 */} + {/* */} +
=> { + const res = await axiosInstance.post(`/group-studies/${groupStudyId}/apply`, { + answer, + }); + + 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 new file mode 100644 index 00000000..91d4b2b6 --- /dev/null +++ b/src/features/study/group/api/group-study-types.ts @@ -0,0 +1,17 @@ +// 그룹 스터디 신청 상태 +type ApplicationStatus = 'PENDING' | 'APPROVED' | 'REJECTED' | 'KICKED'; + +// 그룹 스터디 신청 Request 타입 +export interface ApplyGroupStudyRequest { + groupStudyId: number; + answer: string[]; +} + +// 그룹 스터디 신청 Response 타입 +export interface ApplyGroupStudyResponse { + applyId: number; + applicantId: number; + groupStudyId: number; + status: ApplicationStatus; + createdAt: string; +} diff --git a/src/features/study/group/model/use-apply-group-study.tsx b/src/features/study/group/model/use-apply-group-study.tsx new file mode 100644 index 00000000..f015af51 --- /dev/null +++ b/src/features/study/group/model/use-apply-group-study.tsx @@ -0,0 +1,9 @@ +import { useMutation } from '@tanstack/react-query'; +import { applyGroupStudy } from '../api/apply-group-study'; + +// 그룹 스터디 신청 훅 +export const useApplyGroupStudyMutation = () => { + return useMutation({ + mutationFn: applyGroupStudy, + }); +}; diff --git a/src/features/study/group/ui/apply-group-study-modal.tsx b/src/features/study/group/ui/apply-group-study-modal.tsx index 44c9f106..8fb6abee 100644 --- a/src/features/study/group/ui/apply-group-study-modal.tsx +++ b/src/features/study/group/ui/apply-group-study-modal.tsx @@ -4,19 +4,21 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { XIcon } from 'lucide-react'; import { useState } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; +import { useController, useForm } from 'react-hook-form'; import { z } from 'zod'; import Button from '@/shared/ui/button'; import Checkbox from '@/shared/ui/checkbox'; -import { TextAreaInput } from '@/shared/ui/input'; import { Modal } from '@/shared/ui/modal'; +import { useApplyGroupStudyMutation } from '../model/use-apply-group-study'; interface ApplyGroupStudyModalProps { + groupStudyId: number; title: string; questions: string[]; } export default function ApplyGroupStudyModal({ + groupStudyId, title, questions, }: ApplyGroupStudyModalProps) { @@ -41,6 +43,7 @@ export default function ApplyGroupStudyModal({ setOpen(false)} @@ -52,34 +55,66 @@ export default function ApplyGroupStudyModal({ } const ApplyGroupStudyFormSchema = z.object({ - answer: z.array(z.string().min(1, '답변을 작성해주세요.')), + answer: z.array( + z.string().min(1, '답변을 작성해주세요.'), // 각 항목에 최소 1글자 이상 + ), + agree: z + .boolean() + .refine((val) => val === true, { message: '참여 규칙에 동의해야 합니다.' }), }); -type ApplyGroupStudyFormValues = z.infer; - -function buildApplyGroupStudyDefaultValues(): ApplyGroupStudyFormValues { - return { - answer: [], - }; -} +type ApplyGroupStudyFormData = z.infer; function ApplyGroupStudyForm({ + groupStudyId, title, questions, onClose, }: { + groupStudyId: number; title: string; questions: string[]; onClose: () => void; }) { - const [checked, setChecked] = useState(false); - const methods = useForm<{ answer: string[] }>({ + const { + register, + handleSubmit, + control, + formState: { errors }, + } = useForm({ resolver: zodResolver(ApplyGroupStudyFormSchema), - mode: 'onChange', - defaultValues: buildApplyGroupStudyDefaultValues(), + defaultValues: { + answer: Array(questions.length).fill(''), + }, + }); + + // ✅ checkbox를 controller로 제어 + const { + field: { value: checked, onChange: onToggle }, + } = useController({ + name: 'agree', + control, }); - const handleApply = () => {}; + const { mutate: applyGroupStudy } = useApplyGroupStudyMutation(); + + const onSubmit = (data: ApplyGroupStudyFormData) => { + const { answer } = data; + + console.log(groupStudyId, answer); + + return; + + applyGroupStudy( + { answer, groupStudyId }, + { + onSuccess: () => { + alert('스터디 신청이 완료되었습니다.'); + onClose(); + }, + }, + ); + }; return ( <> @@ -91,23 +126,36 @@ function ApplyGroupStudyForm({ 리더의 질문 - -
- {questions.map((question, index) => ( -
- - + {questions.map((question, index) => ( +
+ + +
+