diff --git a/2024-summer-FE-seminar/packages/web/src/app/layout.js b/2024-summer-FE-seminar/packages/web/src/app/layout.js new file mode 100644 index 0000000..4bec001 --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/app/layout.js @@ -0,0 +1,12 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/2024-summer-FE-seminar/packages/web/src/app/ryan/my/page.tsx b/2024-summer-FE-seminar/packages/web/src/app/ryan/my/page.tsx new file mode 100644 index 0000000..43384bf --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/app/ryan/my/page.tsx @@ -0,0 +1,23 @@ +"use client" + +import FlexWrapper from "@sparcs-clubs/web/common/components/FlexWrapper"; +import PageHead from "@sparcs-clubs/web/common/components/PageHead"; +import MyClubFrame from "@sparcs-clubs/web/features/ryan/frames/MyClubFrame"; +import MyInfoFrame from "@sparcs-clubs/web/features/ryan/frames/MyInfoFrame"; +import MyServiceFrame from "@sparcs-clubs/web/features/ryan/frames/MyServiceFrame"; + +const MyRyan: React.FC = () => ( + + + + + + +); + +export default MyRyan; \ No newline at end of file diff --git a/2024-summer-FE-seminar/packages/web/src/app/ryan/page.tsx b/2024-summer-FE-seminar/packages/web/src/app/ryan/page.tsx new file mode 100644 index 0000000..0ce0490 --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/app/ryan/page.tsx @@ -0,0 +1,102 @@ +/* eslint-disable react/no-children-prop */ + +"use client" + +import { useState } from "react"; + +import styled from "styled-components"; + +import Button from "@sparcs-clubs/web/common/components/Button"; +import Card from "@sparcs-clubs/web/common/components/Card"; +import FlexWrapper from "@sparcs-clubs/web/common/components/FlexWrapper"; +import PageHead from "@sparcs-clubs/web/common/components/PageHead"; +import ItemNumberInput from "@sparcs-clubs/web/common/components/ryan/ItemNumberInput"; +import TextInput from "@sparcs-clubs/web/common/components/ryan/TextInput"; +import Typography from "@sparcs-clubs/web/common/components/Typography"; + + +const MailInput = styled.div` + display: flex; + flex-direction: row; + gap: 10px; + width: 100%; +` + +const RyanAssignmentPage: React.FC = () => { + const [ mailId, setMailId ] = useState(""); + const [ mailDomain, setMailDomain ] = useState(""); + const [ idErrorMessage, setIdErrorMessage ] = useState(""); + const [ domainErrorMessage, setDomainErrorMessage ] = useState(""); + const [ fixDomain, setFixDomain ] = useState(false); + + const validateEmailId = (id: string) => { + const idRegex = /^[a-zA-Z0-9._%+-]+$/; + if (!idRegex.test(id)) { + return '유효한 메일 ID를 입력하세요.'; + } + return ''; + }; + + const validateEmailDomain = (domain: string) => { + const domainRegex = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + if (!domainRegex.test(domain)) { + return '유효한 메일 도메인을 입력하세요.'; + } + return ''; + }; + + return ( + + + + 메일 주소를 입력하세요. + + { + setMailId(value); + setIdErrorMessage(validateEmailId(value)); + } + }/> +
@
+ { + setMailDomain(value); + setDomainErrorMessage(validateEmailDomain(value)) + } + }/> + +
+ + +} + +export default MyInfoFrame; \ No newline at end of file diff --git a/2024-summer-FE-seminar/packages/web/src/features/ryan/frames/MyServiceFrame.tsx b/2024-summer-FE-seminar/packages/web/src/features/ryan/frames/MyServiceFrame.tsx new file mode 100644 index 0000000..f964fe5 --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/features/ryan/frames/MyServiceFrame.tsx @@ -0,0 +1,103 @@ +import AsyncBoundary from "@sparcs-clubs/web/common/components/AsyncBoundary"; +import FlexWrapper from "@sparcs-clubs/web/common/components/FlexWrapper"; +import FoldableSectionTitle from "@sparcs-clubs/web/common/components/FoldableSectionTitle"; +import MoreDetailTitle from "@sparcs-clubs/web/common/components/MoreDetailTitle"; +import MyActivityCertificateTable from "@sparcs-clubs/web/features/my/component/MyActivityCertificateTable"; +import MyCommonSpaceTable from "@sparcs-clubs/web/features/my/component/MyCommonSpaceTable"; +import MyPrintingTable from "@sparcs-clubs/web/features/my/component/MyPrintingTable"; +import MyRentalTable from "@sparcs-clubs/web/features/my/component/MyRentalTable"; + +import { useGetMyActivityCertificate } from "../services/getMyActivityCertificate"; +import { useGetMyCommonSpace } from "../services/getMyCommonSpace"; +import { useGetMyPrinting } from "../services/getMyPrinting"; +import useGetMyRentals from "../services/getMyRentals"; + +const MyServiceFrame = () => { + const startDate = new Date("2024-01-01"); + const endDate = new Date("2024-12-31"); + const pageOffset = 1; + const itemCount = 10; + + const { + data: myRental, + isLoading: rentalLoading, + isError: rentalError, + } = useGetMyRentals(startDate, endDate, pageOffset, itemCount); + + const { + data: myPrinting, + isLoading: printingLoading, + isError: printingError, + } = useGetMyPrinting(startDate, endDate, pageOffset, itemCount); + + const { + data: myActivityCertificate, + isLoading: acfLoading, + isError: acfError, + } = useGetMyActivityCertificate(startDate, endDate, pageOffset, itemCount); + + const { + data: myCommonSpace, + isLoading: cmsLoading, + isError: cmsError, + } = useGetMyCommonSpace(startDate, endDate, pageOffset, itemCount); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +} + +export default MyServiceFrame; \ No newline at end of file diff --git a/2024-summer-FE-seminar/packages/web/src/features/ryan/services/_mock/mockMyClub.ts b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/_mock/mockMyClub.ts new file mode 100644 index 0000000..671f348 --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/_mock/mockMyClub.ts @@ -0,0 +1,282 @@ +import { ActivityCertificateOrderStatusEnum } from "@sparcs-clubs/interface/common/enum/activityCertificate.enum"; +import { CommonSpaceUsageOrderStatusEnum } from "@sparcs-clubs/interface/common/enum/commonSpace.enum"; +import { + PromotionalPrintingOrderStatusEnum, + PromotionalPrintingSizeEnum, +} from "@sparcs-clubs/interface/common/enum/promotionalPrinting.enum"; +import { RentalOrderStatusEnum } from "@sparcs-clubs/interface/common/enum/rental.enum"; + +import type { ApiAcf003ResponseOk } from "@sparcs-clubs/interface/api/activity-certificate/endpoint/apiAcf003"; +import type { ApiCms006ResponseOk } from "@sparcs-clubs/interface/api/common-space/endpoint/apiCms006"; +import type { ApiPrt001ResponseOk } from "@sparcs-clubs/interface/api/promotional-printing/endpoint/apiPrt001"; +import type { ApiRnt003ResponseOK } from "@sparcs-clubs/interface/api/rental/endpoint/apiRnt003"; + +const mockupMyRental: ApiRnt003ResponseOK = { + items: [ + { + id: 1, + studentName: "술박스", + objects: [ + { + id: 1, + name: "돗자리", + number: 3, + }, + { + id: 2, + name: "드릴", + number: 1, + }, + { + id: 3, + name: "어쩌구", + number: 1, + }, + ], + statusEnum: RentalOrderStatusEnum.Applied, + desiredStart: new Date(), + desiredEnd: new Date(), + startDate: new Date(), + endDate: new Date(), + createdAt: new Date(), + }, + { + id: 2, + studentName: "술박스", + objects: [ + { + id: 1, + name: "돗자리", + number: 3, + }, + { + id: 2, + name: "드릴", + number: 1, + }, + { + id: 3, + name: "어쩌구", + number: 1, + }, + ], + statusEnum: RentalOrderStatusEnum.Approved, + desiredStart: new Date(), + desiredEnd: new Date(), + startDate: new Date(), + endDate: new Date(), + createdAt: new Date(), + }, + { + id: 3, + studentName: "술박스", + objects: [ + { + id: 1, + name: "돗자리", + number: 3, + }, + { + id: 2, + name: "드릴", + number: 1, + }, + { + id: 3, + name: "어쩌구", + number: 1, + }, + ], + statusEnum: RentalOrderStatusEnum.Rented, + desiredStart: new Date(), + desiredEnd: new Date(), + startDate: new Date(), + endDate: new Date(), + createdAt: new Date(), + }, + { + id: 4, + studentName: "술박스", + objects: [ + { + id: 1, + name: "돗자리", + number: 3, + }, + { + id: 2, + name: "드릴", + number: 1, + }, + { + id: 3, + name: "어쩌구", + number: 1, + }, + ], + statusEnum: RentalOrderStatusEnum.Returned, + desiredStart: new Date(), + desiredEnd: new Date(), + startDate: new Date(), + endDate: new Date(), + createdAt: new Date(), + }, + ], + total: 4, + offset: 4, +}; + +const mockupMyPrint: ApiPrt001ResponseOk = { + items: [ + { + id: 1, + studentName: "술박스", + status: PromotionalPrintingOrderStatusEnum.Applied, + orders: [ + { + promotionalPrintingSizeEnum: PromotionalPrintingSizeEnum.A4, + numberOfPrints: 50, + }, + { + promotionalPrintingSizeEnum: PromotionalPrintingSizeEnum.A3, + numberOfPrints: 20, + }, + ], + desiredPickUpDate: new Date(), + createdAt: new Date(), + }, + { + id: 2, + studentName: "술박스", + status: PromotionalPrintingOrderStatusEnum.Approved, + orders: [ + { + promotionalPrintingSizeEnum: PromotionalPrintingSizeEnum.A4, + numberOfPrints: 50, + }, + { + promotionalPrintingSizeEnum: PromotionalPrintingSizeEnum.A3, + numberOfPrints: 20, + }, + ], + desiredPickUpDate: new Date(), + createdAt: new Date(), + }, + { + id: 3, + studentName: "술박스", + status: PromotionalPrintingOrderStatusEnum.Printed, + orders: [ + { + promotionalPrintingSizeEnum: PromotionalPrintingSizeEnum.A4, + numberOfPrints: 50, + }, + { + promotionalPrintingSizeEnum: PromotionalPrintingSizeEnum.A3, + numberOfPrints: 20, + }, + ], + desiredPickUpDate: new Date(), + createdAt: new Date(), + }, + { + id: 4, + studentName: "술박스", + status: PromotionalPrintingOrderStatusEnum.Received, + orders: [ + { + promotionalPrintingSizeEnum: PromotionalPrintingSizeEnum.A4, + numberOfPrints: 50, + }, + { + promotionalPrintingSizeEnum: PromotionalPrintingSizeEnum.A3, + numberOfPrints: 20, + }, + ], + desiredPickUpDate: new Date(), + createdAt: new Date(), + }, + ], + total: 4, + offset: 1, +}; + +const mockupMyAcf: ApiAcf003ResponseOk = { + items: [ + { + orderId: 1, + studentName: "술박스", + issuedNumber: 2, + statusEnum: ActivityCertificateOrderStatusEnum.Applied, + createdAt: new Date(), + }, + { + orderId: 2, + studentName: "술박스", + issuedNumber: 3, + statusEnum: ActivityCertificateOrderStatusEnum.Approved, + createdAt: new Date(), + }, + { + orderId: 3, + studentName: "술박스", + issuedNumber: 2, + statusEnum: ActivityCertificateOrderStatusEnum.Issued, + createdAt: new Date(), + }, + { + orderId: 4, + studentName: "술박스", + issuedNumber: 3, + statusEnum: ActivityCertificateOrderStatusEnum.Rejected, + createdAt: new Date(), + }, + ], + total: 4, + offset: 1, +}; + +const mockupMyCms: ApiCms006ResponseOk = { + items: [ + { + orderId: 1, + statusEnum: CommonSpaceUsageOrderStatusEnum.Applied, + spaceName: "제1공용동아리방 (태울관 2101호)", + chargeStudentName: "술박스", + startTerm: new Date(), + endTerm: new Date(), + createdAt: new Date(), + }, + { + orderId: 2, + statusEnum: CommonSpaceUsageOrderStatusEnum.Applied, + spaceName: "제1공용동아리방 (태울관 2101호)", + chargeStudentName: "술박스", + startTerm: new Date(), + endTerm: new Date(), + createdAt: new Date(), + }, + { + orderId: 3, + statusEnum: CommonSpaceUsageOrderStatusEnum.Canceled, + spaceName: "제1공용동아리방 (태울관 2101호)", + chargeStudentName: "술박스", + startTerm: new Date(), + endTerm: new Date(), + createdAt: new Date(), + }, + { + orderId: 4, + statusEnum: CommonSpaceUsageOrderStatusEnum.Used, + spaceName: "제1공용동아리방 (태울관 2101호)", + chargeStudentName: "술박스", + startTerm: new Date(), + endTerm: new Date(), + createdAt: new Date(), + }, + ], + total: 4, + offset: 1, +}; + +export { mockupMyAcf, mockupMyRental, mockupMyPrint, mockupMyCms }; diff --git a/2024-summer-FE-seminar/packages/web/src/features/ryan/services/_mock/mockupUserProfile.ts b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/_mock/mockupUserProfile.ts new file mode 100644 index 0000000..980775a --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/_mock/mockupUserProfile.ts @@ -0,0 +1,16 @@ +import type { ApiUsr001ResponseOK } from "@sparcs-clubs/interface/api/user/endpoint/apiUsr001"; + +const mockupUserProfile: ApiUsr001ResponseOK = { + name: "넙죽이", + studentNumber: 23456789, + clubs: [ + { id: 1, name: "술박스" }, + { id: 2, name: "동연" }, + { id: 3, name: "총학" }, + ], + email: "clubsfighting@sparcs.org", + department: "SoC", + phoneNumber: "010-1234-8765", +}; + +export default mockupUserProfile; diff --git a/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyActivityCertificate.ts b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyActivityCertificate.ts new file mode 100644 index 0000000..ec01573 --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyActivityCertificate.ts @@ -0,0 +1,49 @@ +import apiAcf007 from "@sparcs-clubs/interface/api/activity-certificate/endpoint/apiAcf007"; +import { useQuery } from "@tanstack/react-query"; + +import { + axiosClient, + defineAxiosMock, + UnexpectedAPIResponseError, +} from "@sparcs-clubs/web/lib/axios"; + +import { mockupMyAcf } from "./_mock/mockMyClub"; + +import type { + ApiAcf007RequestQuery, + ApiAcf007ResponseOk, +} from "@sparcs-clubs/interface/api/activity-certificate/endpoint/apiAcf007"; + +export const useGetMyActivityCertificate = ( + startDate: Date, + endDate: Date, + pageOffset: number, + itemCount: number, +) => { + const requestQuery: ApiAcf007RequestQuery = { + startDate, + endDate, + pageOffset, + itemCount, + }; + + return useQuery({ + queryKey: [apiAcf007.url(), requestQuery], + queryFn: async (): Promise => { + const { data, status } = await axiosClient.get(apiAcf007.url(), { + params: requestQuery, + }); + + switch (status) { + case 200: + return apiAcf007.responseBodyMap[200].parse(data); + default: + throw new UnexpectedAPIResponseError(); + } + }, + }); +}; + +defineAxiosMock(mock => { + mock.onGet(apiAcf007.url()).reply(() => [200, mockupMyAcf]); +}); diff --git a/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyCommonSpace.ts b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyCommonSpace.ts new file mode 100644 index 0000000..c60727b --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyCommonSpace.ts @@ -0,0 +1,49 @@ +import apiCms007 from "@sparcs-clubs/interface/api/common-space/endpoint/apiCms007"; +import { useQuery } from "@tanstack/react-query"; + +import { + axiosClient, + defineAxiosMock, + UnexpectedAPIResponseError, +} from "@sparcs-clubs/web/lib/axios"; + +import { mockupMyCms } from "./_mock/mockMyClub"; + +import type { + ApiCms007RequestQuery, + ApiCms007ResponseOk, +} from "@sparcs-clubs/interface/api/common-space/endpoint/apiCms007"; + +export const useGetMyCommonSpace = ( + startDate: Date, + endDate: Date, + pageOffset: number, + itemCount: number, +) => { + const requestQuery: ApiCms007RequestQuery = { + startDate, + endDate, + pageOffset, + itemCount, + }; + + return useQuery({ + queryKey: [apiCms007.url(), requestQuery], + queryFn: async (): Promise => { + const { data, status } = await axiosClient.get(apiCms007.url(), { + params: requestQuery, + }); + + switch (status) { + case 200: + return apiCms007.responseBodyMap[200].parse(data); + default: + throw new UnexpectedAPIResponseError(); + } + }, + }); +}; + +defineAxiosMock(mock => { + mock.onGet(apiCms007.url()).reply(() => [200, mockupMyCms]); +}); diff --git a/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyPrinting.ts b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyPrinting.ts new file mode 100644 index 0000000..21f093d --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyPrinting.ts @@ -0,0 +1,49 @@ +import apiPrt005 from "@sparcs-clubs/interface/api/promotional-printing/endpoint/apiPrt005"; +import { useQuery } from "@tanstack/react-query"; + +import { + axiosClient, + defineAxiosMock, + UnexpectedAPIResponseError, +} from "@sparcs-clubs/web/lib/axios"; + +import { mockupMyPrint } from "./_mock/mockMyClub"; + +import type { + ApiPrt005RequestQuery, + ApiPrt005ResponseOk, +} from "@sparcs-clubs/interface/api/promotional-printing/endpoint/apiPrt005"; + +export const useGetMyPrinting = ( + startDate: Date, + endDate: Date, + pageOffset: number, + itemCount: number, +) => { + const requestQuery: ApiPrt005RequestQuery = { + startDate, + endDate, + pageOffset, + itemCount, + }; + + return useQuery({ + queryKey: [apiPrt005.url(), requestQuery], + queryFn: async (): Promise => { + const { data, status } = await axiosClient.get(apiPrt005.url(), { + params: requestQuery, + }); + + switch (status) { + case 200: + return apiPrt005.responseBodyMap[200].parse(data); + default: + throw new UnexpectedAPIResponseError(); + } + }, + }); +}; + +defineAxiosMock(mock => { + mock.onGet(apiPrt005.url()).reply(() => [200, mockupMyPrint]); +}); diff --git a/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyRentals.ts b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyRentals.ts new file mode 100644 index 0000000..8c2f296 --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/getMyRentals.ts @@ -0,0 +1,51 @@ +import apiRnt006 from "@sparcs-clubs/interface/api/rental/endpoint/apiRnt006"; +import { useQuery } from "@tanstack/react-query"; + +import { + axiosClient, + defineAxiosMock, + UnexpectedAPIResponseError, +} from "@sparcs-clubs/web/lib/axios"; + +import { mockupMyRental } from "./_mock/mockMyClub"; + +import type { + ApiRnt006RequestQuery, + ApiRnt006ResponseOK, +} from "@sparcs-clubs/interface/api/rental/endpoint/apiRnt006"; + +const useGetMyRentals = ( + startDate: Date, + endDate: Date, + pageOffset: number, + itemCount: number, +) => { + const requestQuery: ApiRnt006RequestQuery = { + startDate, + endDate, + pageOffset, + itemCount, + }; + + return useQuery({ + queryKey: [apiRnt006.url(), requestQuery], + queryFn: async (): Promise => { + const { data, status } = await axiosClient.get(apiRnt006.url(), { + params: requestQuery, + }); + + switch (status) { + case 200: + return apiRnt006.responseBodyMap[200].parse(data); + default: + throw new UnexpectedAPIResponseError(); + } + }, + }); +}; + +defineAxiosMock(mock => { + mock.onGet(apiRnt006.url()).reply(() => [200, mockupMyRental]); +}); + +export default useGetMyRentals; \ No newline at end of file diff --git a/2024-summer-FE-seminar/packages/web/src/features/ryan/services/userProfile.ts b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/userProfile.ts new file mode 100644 index 0000000..fe18fb2 --- /dev/null +++ b/2024-summer-FE-seminar/packages/web/src/features/ryan/services/userProfile.ts @@ -0,0 +1,26 @@ +import apiUsr001, { ApiUsr001ResponseOK } from "@sparcs-clubs/interface/api/user/endpoint/apiUsr001"; + +import { useQuery } from "@tanstack/react-query"; + +import { axiosClient, defineAxiosMock, UnexpectedAPIResponseError } from "@sparcs-clubs/web/lib/axios"; + +import mockupUserProfile from "./_mock/mockupUserProfile"; + +export const useGetUserProfile = () => + useQuery({ + queryKey: [apiUsr001.url()], + queryFn: async (): Promise => { + const { data, status } = await axiosClient.get(apiUsr001.url(), {}); + + switch (status) { + case 200: + return apiUsr001.responseBodyMap[200].parse(data); + default: + throw new UnexpectedAPIResponseError(); + } + } + }); + +defineAxiosMock(mock => { + mock.onGet(apiUsr001.url()).reply(() => [200, mockupUserProfile]); +}); \ No newline at end of file diff --git a/2024-summer-zod-semina/interface/db.ts b/2024-summer-zod-semina/interface/db.ts index c0c6f6e..607b0ac 100644 --- a/2024-summer-zod-semina/interface/db.ts +++ b/2024-summer-zod-semina/interface/db.ts @@ -3,17 +3,20 @@ import { z } from "zod"; export const dbElement = z.object({ // TODO 2: mockDB에 들어갈 데이터의 타입을 정의해 주세요 // { value: 0 이샹의 정수, updatedAt: ts date 타입 } + value: z.coerce.number().min(0), updatedAt: z.coerce.date() }); export const twoPositiveIntegerParser = z.object({ // TODO 1: 아래와 같이 2개의 양의 정수를 포함한 객체를 parsing하는 zod object를 구현하세요 // { a: 양의 정수, b: 양의 정수 } + a: z.coerce.number().min(1), b: z.coerce.number().min(1) }); export const getDbIndexParser = (dbLength: number) => z.object({ // db의 크기를 받아 db 범위 내의 인덱스만을 값으로 받는 parser를 만들어 주세요 // { index: int, 0 <= index <= db_max_index} + index: z.coerce.number().min(0).max(dbLength - 1) }); export const dbPaginationQueryParser = z.object({ @@ -22,4 +25,9 @@ export const dbPaginationQueryParser = z.object({ // pageOffset: int, >= 1 // itemCount: int, >= 1 // }, + + startDate: z.optional(z.coerce.date()), + endDate: z.optional(z.coerce.date()), + pageOffset: z.coerce.number().min(1), + itemCount: z.coerce.number().min(1) });