diff --git a/apps/admin/apis/study/studyApi.ts b/apps/admin/apis/study/studyApi.ts index d9a060b9..cd1de72d 100644 --- a/apps/admin/apis/study/studyApi.ts +++ b/apps/admin/apis/study/studyApi.ts @@ -2,7 +2,10 @@ import { fetcher } from "@wow-class/utils"; import { apiPath, mentorApiPath } from "constants/apiPath"; import { tags } from "constants/tags"; import type { AnnouncementApiResponseDto } from "types/dtos/announcement"; -import type { AssignmentApiResponseDto } from "types/dtos/assignmentList"; +import type { + AssignmentApiRequestDto, + AssignmentApiResponseDto, +} from "types/dtos/assignmentList"; import type { CurriculumApiResponseDto } from "types/dtos/curriculumList"; import type { StudyBasicInfoApiResponseDto } from "types/dtos/studyBasicInfo"; import type { StudyAnnouncementType } from "types/entities/study"; @@ -40,6 +43,36 @@ export const studyApi = { ); return response.data; }, + getAssignment: async (studyDetailId: number) => { + const response = await fetcher.get( + `/mentor/study-details/${studyDetailId}/assignments`, + { + next: { tags: [`${tags.assignments} ${studyDetailId}`] }, + cache: "force-cache", + } + ); + return response.data; + }, + createAssignment: async ( + studyDetailId: number, + data: AssignmentApiRequestDto + ) => { + const response = await fetcher.put( + `/mentor/study-details/${studyDetailId}/assignments`, + data + ); + return { success: response.ok }; + }, + patchAssignment: async ( + studyDetailId: number, + data: AssignmentApiRequestDto + ) => { + const response = await fetcher.patch( + `/mentor/study-details/${studyDetailId}/assignments`, + data + ); + return { success: response.ok }; + }, cancelAssignment: async (studyDetailId: number) => { const response = await fetcher.patch( `/mentor/study-details/${studyDetailId}/assignments/cancel`, diff --git a/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentButtons.tsx b/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentButtons.tsx new file mode 100644 index 00000000..9d4b92df --- /dev/null +++ b/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentButtons.tsx @@ -0,0 +1,68 @@ +"use client"; +import { Flex } from "@styled-system/jsx"; +import { studyApi } from "apis/study/studyApi"; +import { routerPath } from "constants/router/routerPath"; +import Link from "next/link"; +import type { StudyAssignmentStatusType } from "types/entities/study"; +import Button from "wowds-ui/Button"; + +const AssignmentButtons = ({ + studyDetailId, + assignmentStatus, +}: { + studyDetailId: number; + assignmentStatus: StudyAssignmentStatusType; +}) => { + const handleCancelAssignment = async () => { + const { success } = await studyApi.cancelAssignment(studyDetailId); + if (success) { + console.log("휴강 처리에 성공했어요."); + } else { + console.log("휴강 처리에 실패했어요."); + } + }; + + if (assignmentStatus === "OPEN") { + return ( + + ); + } + + if (assignmentStatus === "CANCELLED") { + return ( + + + + + ); + } + + return ( + + + + + ); +}; + +export default AssignmentButtons; diff --git a/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentListItem.tsx b/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentListItem.tsx index 7b6040f0..10e7f3cd 100644 --- a/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentListItem.tsx +++ b/apps/admin/app/studies/[studyId]/_components/assignment/AssignmentListItem.tsx @@ -1,43 +1,23 @@ -"use client"; import { cva } from "@styled-system/css"; import { Flex } from "@styled-system/jsx"; import { Table, Text } from "@wow-class/ui"; import { padWithZero, parseISODate } from "@wow-class/utils"; -import { studyApi } from "apis/study/studyApi"; -import { tags } from "constants/tags"; -import Link from "next/link"; import type { AssignmentApiResponseDto } from "types/dtos/assignmentList"; import getIsCurrentWeek from "utils/getIsCurrentWeek"; -import { revalidateTagByName } from "utils/revalidateTagByName"; -import Button from "wowds-ui/Button"; + +import AssignmentButtons from "./AssignmentButtons"; const AssignmentListItem = ({ assignment, }: { assignment: AssignmentApiResponseDto; }) => { - const { - studyDetailId, - title, - deadline, - week, - descriptionLink, - assignmentStatus, - } = assignment; + const { studyDetailId, title, deadline, week, assignmentStatus } = assignment; const thisWeekAssignment = getIsCurrentWeek(deadline); const { year, month, day, hours, minutes } = parseISODate(deadline); const studyDeadline = `종료 : ${year}년 ${month}월 ${day}일 ${padWithZero(hours)}:${padWithZero(minutes)}`; - const handleCancelAssignment = async (studyDetailId: number) => { - const { success } = await studyApi.cancelAssignment(studyDetailId); - if (success) { - window.alert("휴강 처리에 성공했어요."); - revalidateTagByName(tags.assignments); - } else { - window.alert("휴강 처리에 실패했어요."); - } - }; return ( @@ -57,38 +37,10 @@ const AssignmentListItem = ({ - <> - {assignmentStatus === "OPEN" ? ( - - ) : ( - - - - - )} - +
); diff --git a/apps/admin/app/studies/[studyId]/_components/assignment/CancelStudyButton.tsx b/apps/admin/app/studies/[studyId]/_components/assignment/CancelStudyButton.tsx deleted file mode 100644 index d29b64f9..00000000 --- a/apps/admin/app/studies/[studyId]/_components/assignment/CancelStudyButton.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; -import { Flex } from "@styled-system/jsx"; -import { studyApi } from "apis/study/studyApi"; -import { useRouter } from "next/navigation"; -import type { StudyAssignmentStatusType } from "types/entities/study"; -import Button from "wowds-ui/Button"; - -const CancelStudyButton = ({ - descriptionLink, - studyDetailId, - assignmentStatus, -}: { - descriptionLink: string; - studyDetailId: number; - assignmentStatus: StudyAssignmentStatusType; -}) => { - const router = useRouter(); - const handleCancelAssignment = async (studyDetailId: number) => { - const { success } = await studyApi.cancelAssignment(studyDetailId); - if (success) { - console.log("휴강 처리에 성공했어요."); - } else { - console.log("휴강 처리에 실패했어요."); - } - }; - return ( - <> - {assignmentStatus === "OPEN" ? ( - - ) : ( - - - - - )} - - ); -}; - -export default CancelStudyButton; diff --git a/apps/admin/app/studies/[studyId]/_components/header/Header.tsx b/apps/admin/app/studies/[studyId]/_components/header/Header.tsx index 5c64473f..89df9166 100644 --- a/apps/admin/app/studies/[studyId]/_components/header/Header.tsx +++ b/apps/admin/app/studies/[studyId]/_components/header/Header.tsx @@ -5,8 +5,8 @@ import { Flex } from "@styled-system/jsx"; import { Space, Text } from "@wow-class/ui"; import { padWithZero, parseISODate } from "@wow-class/utils"; import { studyApi } from "apis/study/studyApi"; +import ItemSeparator from "components/ItemSeparator"; import { dayToKorean } from "constants/dayToKorean"; -import Image from "next/image"; import Link from "next/link"; import { useEffect, useState } from "react"; import type { StudyBasicInfoApiResponseDto } from "types/dtos/studyBasicInfo"; @@ -114,11 +114,11 @@ const Header = ({ {studySemester} - + {mentorName} 멘토 - + {studyType} @@ -138,13 +138,13 @@ const Header = ({ {studySchedule()} - + )} {totalWeek}주 코스 - + {studyPeriod} @@ -175,10 +175,6 @@ const Header = ({ export default Header; -const ItemSeparator = () => ( - item separator -); - const downArrowIconStyle = css({ cursor: "pointer", }); diff --git a/apps/admin/app/studies/assignments/[studyDetailId]/_components/AssignmentForm.tsx b/apps/admin/app/studies/assignments/[studyDetailId]/_components/AssignmentForm.tsx new file mode 100644 index 00000000..840d930a --- /dev/null +++ b/apps/admin/app/studies/assignments/[studyDetailId]/_components/AssignmentForm.tsx @@ -0,0 +1,41 @@ +import { Flex } from "@styled-system/jsx"; +import { useFormContext } from "react-hook-form"; +import type { + AssignmentApiRequestDto, + AssignmentApiResponseDto, +} from "types/dtos/assignmentList"; + +import CustomTextField from "./CustomTextField"; + +const AssignmentForm = ({ + assignment, +}: { + assignment: AssignmentApiResponseDto; +}) => { + const { control } = useFormContext(); + const { title, descriptionLink } = assignment; + + // TODO: Picker 컴포넌트 추가 + return ( + + + + + ); +}; + +export default AssignmentForm; diff --git a/apps/admin/app/studies/assignments/[studyDetailId]/_components/AssignmentHeader.tsx b/apps/admin/app/studies/assignments/[studyDetailId]/_components/AssignmentHeader.tsx new file mode 100644 index 00000000..f2803f10 --- /dev/null +++ b/apps/admin/app/studies/assignments/[studyDetailId]/_components/AssignmentHeader.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { css } from "@styled-system/css"; +import { Flex } from "@styled-system/jsx"; +import { Text } from "@wow-class/ui"; +import { studyApi } from "apis/study/studyApi"; +import { tags } from "constants/tags"; +import { useFormContext } from "react-hook-form"; +import type { + AssignmentApiRequestDto, + AssignmentApiResponseDto, +} from "types/dtos/assignmentList"; +import { revalidateTagByName } from "utils/revalidateTagByName"; +import Button from "wowds-ui/Button"; + +interface AssignmentHeaderProps { + assignment: AssignmentApiResponseDto; + disabled: boolean; +} + +const AssignmentHeader = ({ assignment, disabled }: AssignmentHeaderProps) => { + const { studyDetailId, week, assignmentStatus } = assignment; + const methods = useFormContext< + AssignmentApiRequestDto & { + onOpen: () => void; + } + >(); + + const onOpen = methods.getValues("onOpen"); + + const handleClickSubmit = async () => { + if (assignmentStatus === "CANCELLED") return; + + const data = { + title: methods.getValues("title"), + descriptionNotionLink: methods.getValues("descriptionNotionLink"), + deadLine: methods.getValues("deadLine"), + }; + + const { success } = + assignmentStatus === "NONE" + ? await studyApi.createAssignment(studyDetailId, data) + : await studyApi.patchAssignment(studyDetailId, data); + if (success) { + revalidateTagByName(`${tags.assignments} ${studyDetailId}`); + revalidateTagByName(tags.assignments); + onOpen(); + } + }; + + return ( +
+ + + 과제 정보를 입력해주세요 + + + {week}주차 과제 + + + +
+ ); +}; + +const headerStyle = css({ + width: "100%", + display: "flex", + alignItems: "top", + justifyContent: "space-between", +}); + +export default AssignmentHeader; diff --git a/apps/admin/app/studies/assignments/[studyDetailId]/_components/CustomTextField.tsx b/apps/admin/app/studies/assignments/[studyDetailId]/_components/CustomTextField.tsx new file mode 100644 index 00000000..28979eba --- /dev/null +++ b/apps/admin/app/studies/assignments/[studyDetailId]/_components/CustomTextField.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useController } from "react-hook-form"; +import type { TextFieldProps } from "wowds-ui/TextField"; +import TextField from "wowds-ui/TextField"; + +interface CustomTextFieldProps extends TextFieldProps { + name: string; + control: any; +} + +const CustomTextField = ({ + name, + control, + defaultValue, + ...rest +}: CustomTextFieldProps) => { + const { field } = useController({ + name, + control, + rules: { required: true }, + defaultValue, + }); + + return ( + + ); +}; + +export default CustomTextField; diff --git a/apps/admin/app/studies/assignments/[studyDetailId]/_components/SuccessModal.tsx b/apps/admin/app/studies/assignments/[studyDetailId]/_components/SuccessModal.tsx new file mode 100644 index 00000000..fa8d1421 --- /dev/null +++ b/apps/admin/app/studies/assignments/[studyDetailId]/_components/SuccessModal.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Flex, styled } from "@styled-system/jsx"; +import { Modal, Text } from "@wow-class/ui"; +import Link from "next/link"; +import Button from "wowds-ui/Button"; + +interface SuccesModalProps { + studyName: string; + week: number; + type: "개설" | "수정"; + studyDetailId: string; +} + +const SuccessModal = ({ + studyName, + week, + type, + studyDetailId, +}: SuccesModalProps) => { + return ( + + + + + {studyName} {week}주차 + +
+ 과제가 {type}되었어요 +
+ +
+
+ ); +}; + +export default SuccessModal; diff --git a/apps/admin/app/studies/assignments/[studyDetailId]/edit-assignment/page.tsx b/apps/admin/app/studies/assignments/[studyDetailId]/edit-assignment/page.tsx new file mode 100644 index 00000000..6be479aa --- /dev/null +++ b/apps/admin/app/studies/assignments/[studyDetailId]/edit-assignment/page.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useOpenState } from "@wow-class/ui/hooks"; +import { studyApi } from "apis/study/studyApi"; +import { assignmentStatusMap } from "constants/assignmentStatusMap"; +import { useEffect, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import type { + AssignmentApiRequestDto, + AssignmentApiResponseDto, +} from "types/dtos/assignmentList"; + +import AssignmentForm from "../_components/AssignmentForm"; +import AssignmentHeader from "../_components/AssignmentHeader"; +import SuccessModal from "../_components/SuccessModal"; + +const Assignments = ({ + params: { studyDetailId }, +}: { + params: { study: string; studyDetailId: string }; +}) => { + const { open, onOpen } = useOpenState(); + + const [assignment, setAssignment] = useState( + null + ); + + useEffect(() => { + const fetchAssignment = async () => { + if (studyDetailId) { + const data = await studyApi.getAssignment(+studyDetailId); + if (data) setAssignment(data); + } + }; + fetchAssignment(); + }, [studyDetailId]); + + const methods = useForm< + AssignmentApiRequestDto & { + onOpen: () => void; + } + >({ + defaultValues: { + title: assignment?.title, + deadLine: "2024-09-07T00:00:00", + descriptionNotionLink: assignment?.descriptionLink, + onOpen: onOpen, + }, + }); + + if (!assignment) return null; + const { assignmentStatus, week } = assignment; + + // TODO: 휴강된 경우 진입 막기 + if (assignmentStatus === "CANCELLED") return null; + + // TODO: studyName 추가 + return ( + <> + {open && ( + + )} + + + + + + ); +}; + +export default Assignments; diff --git a/apps/admin/app/studies/assignments/[studyDetailId]/page.tsx b/apps/admin/app/studies/assignments/[studyDetailId]/page.tsx new file mode 100644 index 00000000..ec89fc91 --- /dev/null +++ b/apps/admin/app/studies/assignments/[studyDetailId]/page.tsx @@ -0,0 +1,85 @@ +import { css } from "@styled-system/css"; +import { Flex, styled } from "@styled-system/jsx"; +import { Text } from "@wow-class/ui"; +import { padWithZero, parseISODate } from "@wow-class/utils"; +import { studyApi } from "apis/study/studyApi"; +import ItemSeparator from "components/ItemSeparator"; +import { routerPath } from "constants/router/routerPath"; +import Link from "next/link"; +import Button from "wowds-ui/Button"; +import TextButton from "wowds-ui/TextButton"; + +const AssignmentsPage = async ({ + params: { studyDetailId }, +}: { + params: { studyDetailId: string }; +}) => { + const assignment = await studyApi.getAssignment(+studyDetailId); + if (!assignment) return null; + + const { week, title, descriptionLink, deadline } = assignment; + + const { year, month, day, hours, minutes } = parseISODate(deadline); + + // TODO: studyName 추가 + return ( + <> +
+ + {title} + + {week}주차 과제 + + + +
+ + + 과제 제목 + + {title} + + + + 과제 명세 링크 + + + + 과제 기한 + + {year}년 {month}월 {day}일 {hours}:{padWithZero(minutes)} + + + + + ); +}; + +const headerStyle = css({ + width: "100%", + display: "flex", + justifyContent: "space-between", +}); + +const textButtonStyle = css({ + color: "sub", +}); + +export default AssignmentsPage; diff --git a/apps/admin/app/studies/assignments/layout.tsx b/apps/admin/app/studies/assignments/layout.tsx new file mode 100644 index 00000000..e79da0ff --- /dev/null +++ b/apps/admin/app/studies/assignments/layout.tsx @@ -0,0 +1,15 @@ +import { Flex } from "@styled-system/jsx"; + +const AssignmentsLayout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return ( + + {children} + + ); +}; + +export default AssignmentsLayout; diff --git a/apps/admin/app/studies/create-study/@modal/(.)created-study-check/page.tsx b/apps/admin/app/studies/create-study/@modal/(.)created-study-check/page.tsx index ff50c399..4b2bcbbe 100644 --- a/apps/admin/app/studies/create-study/@modal/(.)created-study-check/page.tsx +++ b/apps/admin/app/studies/create-study/@modal/(.)created-study-check/page.tsx @@ -4,10 +4,10 @@ import { Flex } from "@styled-system/jsx"; import { Modal, Space, Text } from "@wow-class/ui"; import { useModalRoute } from "@wow-class/ui/hooks"; import { createStudyApi } from "apis/study/createStudyApi"; +import ItemSeparator from "components/ItemSeparator"; import { routerPath } from "constants/router/routerPath"; import { tags } from "constants/tags"; import useParseSearchParams from "hooks/useParseSearchParams"; -import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import type { CreateStudyApiRequestDto } from "types/dtos/createStudy"; import { revalidateTagByName } from "utils/revalidateTagByName"; @@ -45,7 +45,7 @@ const CreatedStudyCheckModal = () => { {studyName} - + {semester} @@ -64,7 +64,3 @@ const CreatedStudyCheckModal = () => { }; export default CreatedStudyCheckModal; - -const ItemSeparator = () => ( - item separator -); diff --git a/apps/admin/components/ItemSeparator.tsx b/apps/admin/components/ItemSeparator.tsx new file mode 100644 index 00000000..4c8cc0e6 --- /dev/null +++ b/apps/admin/components/ItemSeparator.tsx @@ -0,0 +1,18 @@ +import Image from "next/image"; + +const ItemSeparator = ({ + width, + height, +}: { + width: number; + height: number; +}) => ( + item separator +); + +export default ItemSeparator; diff --git a/apps/admin/constants/assignmentStatusMap.ts b/apps/admin/constants/assignmentStatusMap.ts new file mode 100644 index 00000000..1b72d005 --- /dev/null +++ b/apps/admin/constants/assignmentStatusMap.ts @@ -0,0 +1,10 @@ +import type { AssignmentStatusType } from "types/entities/assignment"; + +export const assignmentStatusMap: Record< + AssignmentStatusType, + "개설" | "수정" +> = { + NONE: "개설", + OPEN: "수정", + CANCELLED: "개설", +}; diff --git a/apps/admin/constants/router/routerPath.ts b/apps/admin/constants/router/routerPath.ts index 02c65fae..b8415d60 100644 --- a/apps/admin/constants/router/routerPath.ts +++ b/apps/admin/constants/router/routerPath.ts @@ -29,4 +29,14 @@ export const routerPath = { description: "스터디 생성을 확인하는 모달창입니다.", href: "create-study/created-study-check", }, + "assignment-detail": { + description: "과제 내용 보기 페이지로 이동합니다.", + href: (studyDetailId: number | string) => + `/studies/assignments/${studyDetailId}`, + }, + "assignment-edit": { + description: "과제 개설/수정 페이지로 이동합니다.", + href: (studyDetailId: number | string) => + `/studies/assignments/${studyDetailId}/edit-assignment`, + }, }; diff --git a/apps/admin/package.json b/apps/admin/package.json index 965ec71a..22835f4f 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -17,7 +17,7 @@ "react-clock": "^5.0.0", "react-day-picker": "^9.0.8", "react-dom": "^18.3.1", - "react-hook-form": "^7.52.2" + "react-hook-form": "^7.53.0" }, "devDependencies": { "@types/node": "^20", diff --git a/apps/admin/types/dtos/assignmentList.ts b/apps/admin/types/dtos/assignmentList.ts index 61522410..420fe694 100644 --- a/apps/admin/types/dtos/assignmentList.ts +++ b/apps/admin/types/dtos/assignmentList.ts @@ -8,3 +8,9 @@ export interface AssignmentApiResponseDto { descriptionLink: string; assignmentStatus: StudyAssignmentStatusType; } + +export interface AssignmentApiRequestDto { + title: string; + descriptionNotionLink: string; + deadLine: string; +} diff --git a/apps/admin/types/entities/assignment.ts b/apps/admin/types/entities/assignment.ts new file mode 100644 index 00000000..936248af --- /dev/null +++ b/apps/admin/types/entities/assignment.ts @@ -0,0 +1 @@ +export type AssignmentStatusType = "NONE" | "OPEN" | "CANCELLED"; diff --git a/packages/ui/src/components/Modal/index.tsx b/packages/ui/src/components/Modal/index.tsx index b6b5752c..e5e363d8 100644 --- a/packages/ui/src/components/Modal/index.tsx +++ b/packages/ui/src/components/Modal/index.tsx @@ -4,7 +4,7 @@ import { css } from "@styled-system/css"; import { Flex, styled } from "@styled-system/jsx"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import type { CSSProperties, PropsWithChildren } from "react"; +import { type CSSProperties, forwardRef, type PropsWithChildren } from "react"; import closeUrl from "../../assets/images/close.svg"; import { useClickOutside } from "../../hooks"; @@ -24,7 +24,7 @@ export interface ModalProps extends PropsWithChildren { className?: string; } -const Modal = ({ children, onClose, ...rest }: ModalProps) => { +const Modal = forwardRef(({ children, onClose, ...rest }: ModalProps) => { const router = useRouter(); const handleClose = onClose || router.back; @@ -46,7 +46,7 @@ const Modal = ({ children, onClose, ...rest }: ModalProps) => { ); -}; +}); const dialogStyle = css({ width: "40.75rem", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 425c0844..94b6cb3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,7 +70,7 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-hook-form: - specifier: ^7.52.2 + specifier: ^7.53.0 version: 7.53.0(react@18.3.1) devDependencies: '@types/node':