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 RyanAssignmentPage;
\ No newline at end of file
diff --git a/2024-summer-FE-seminar/packages/web/src/common/components/ryan/ItemNumberInput.tsx b/2024-summer-FE-seminar/packages/web/src/common/components/ryan/ItemNumberInput.tsx
new file mode 100644
index 0000000..dd4a7b2
--- /dev/null
+++ b/2024-summer-FE-seminar/packages/web/src/common/components/ryan/ItemNumberInput.tsx
@@ -0,0 +1,126 @@
+/* eslint-disable react/prop-types */
+
+import { ChangeEvent, InputHTMLAttributes, useState } from "react";
+
+import styled, { css } from "styled-components";
+
+import FormError from "../FormError";
+import Typography from "../Typography";
+
+interface TextInputProps
+ extends InputHTMLAttributes {
+ placeholder: string
+ maxValue: number
+ unitString: string
+ handleChange: (value: number) => void
+}
+
+
+const errorBorderStyle = css`
+ border-color: ${({ theme }) => theme.colors.RED[600]};
+`;
+
+const StyledNumberInput = styled.div`
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ padding: 8px 12px 8px 12px;
+ border: 1px solid ${({ theme }) => theme.colors.GRAY[200]};
+ border-radius: 4px;
+ gap: 8px;
+ color: ${({ theme }) => theme.colors.BLACK};
+ background-color: ${({ theme }) => theme.colors.WHITE};
+ &:focus {
+ border-color: ${({ theme, hasError, disabled }) =>
+ !hasError && !disabled && theme.colors.PRIMARY};
+ }
+ &:hover:not(:focus) {
+ border-color: ${({ theme, hasError, disabled }) =>
+ !hasError && !disabled && theme.colors.GRAY[300]};
+ }
+ ${({ hasError }) => hasError && errorBorderStyle}
+`;
+
+const StyledInput = styled.input`
+ outline: none;
+ border: none;
+ flex: 1;
+ font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD};
+ font-size: 16px;
+ line-height: 20px;
+ font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR};
+ &::placeholder {
+ color: ${({ theme }) => theme.colors.GRAY[200]};
+ }
+`
+
+const StyledInputWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+`
+
+const ItemNumberInput: React.FC = ({
+ placeholder = "",
+ maxValue = 0,
+ unitString = "개",
+ handleChange = () => {},
+ ...props
+}) => {
+ const [ value, setValue ] = useState(0);
+ const [ errorMessage, setErrorMessage ] = useState("");
+
+ function validate(inputValue: number): string {
+ if (inputValue > maxValue)
+ return "신청 가능 개수를 초과했습니다"
+ return ""
+ }
+
+ const onValueChange = (e: ChangeEvent) => {
+ const inputString = e.target.value;
+ const inputStringWithoutUnit =
+ inputString.endsWith(unitString)
+ ? inputString.slice(0, -unitString.length)
+ : inputString;
+ const inputValue = Number(inputStringWithoutUnit);
+
+ if(Number.isNaN(inputValue)) {
+ setValue(value);
+ setErrorMessage("숫자만 입력 가능합니다");
+ } else {
+ setValue(inputValue);
+ setErrorMessage(validate(inputValue));
+ }
+
+ handleChange(inputValue);
+ }
+
+ function valueString(): string {
+ if (value === 0) return "";
+ return `${value}${unitString}`;
+ }
+
+ return (
+
+
+
+ {`/ ${maxValue}${unitString}`}
+
+ {errorMessage && {errorMessage}}
+
+ );
+};
+
+export default ItemNumberInput;
+
diff --git a/2024-summer-FE-seminar/packages/web/src/common/components/ryan/TextInput.tsx b/2024-summer-FE-seminar/packages/web/src/common/components/ryan/TextInput.tsx
new file mode 100644
index 0000000..d904a07
--- /dev/null
+++ b/2024-summer-FE-seminar/packages/web/src/common/components/ryan/TextInput.tsx
@@ -0,0 +1,93 @@
+/* eslint-disable react/prop-types */
+import { ChangeEvent, InputHTMLAttributes } from "react"
+
+import styled, { css } from "styled-components";
+
+import FormError from "../FormError";
+
+interface TextInputProps
+ extends InputHTMLAttributes {
+ placeholder: string
+ disabled: boolean
+ value: string
+ errorMessage: string
+ handleChange: (value: string) => void
+}
+
+
+const errorBorderStyle = css`
+ border-color: ${({ theme }) => theme.colors.RED[600]};
+`;
+
+const disabledStyle = css`
+ background-color: ${({ theme }) => theme.colors.GRAY[100]};
+ border-color: ${({ theme }) => theme.colors.GRAY[200]};
+`;
+
+const StyledInput = styled.input`
+ display: block;
+ width: 100%;
+ padding: 8px 12px 8px 12px;
+ outline: none;
+ border: 1px solid ${({ theme }) => theme.colors.GRAY[200]};
+ border-radius: 4px;
+ gap: 8px;
+ font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD};
+ font-size: 16px;
+ line-height: 20px;
+ font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR};
+ color: ${({ theme }) => theme.colors.BLACK};
+ background-color: ${({ theme }) => theme.colors.WHITE};
+ &:focus {
+ border-color: ${({ theme, hasError, disabled }) =>
+ !hasError && !disabled && theme.colors.PRIMARY};
+ }
+ &:hover:not(:focus) {
+ border-color: ${({ theme, hasError, disabled }) =>
+ !hasError && !disabled && theme.colors.GRAY[300]};
+ }
+ &::placeholder {
+ color: ${({ theme }) => theme.colors.GRAY[200]};
+ }
+ ${({ disabled }) => disabled && disabledStyle}
+ ${({ hasError }) => hasError && errorBorderStyle}
+`;
+
+const StyledInputWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+`
+
+const TextInput: React.FC = ({
+ placeholder = "",
+ value = "",
+ errorMessage = "",
+ disabled = false,
+ handleChange = () => {},
+ ...props
+}) => {
+ const onValueChange = (e: ChangeEvent) => {
+ const inputValue = e.target.value;
+ handleChange(inputValue);
+ }
+
+ return (
+
+
+ {errorMessage && {errorMessage}}
+
+ );
+};
+
+export default TextInput;
+
diff --git a/2024-summer-FE-seminar/packages/web/src/features/ryan/frames/MyClubFrame.tsx b/2024-summer-FE-seminar/packages/web/src/features/ryan/frames/MyClubFrame.tsx
new file mode 100644
index 0000000..2aae93a
--- /dev/null
+++ b/2024-summer-FE-seminar/packages/web/src/features/ryan/frames/MyClubFrame.tsx
@@ -0,0 +1,29 @@
+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 ClubListGrid from "@sparcs-clubs/web/features/clubs/components/ClubListGrid";
+import useGetMyClub from "@sparcs-clubs/web/features/my/clubs/service/useGetMyClub";
+
+const MyClubFrame = () => {
+ const { data, isLoading, isError } = useGetMyClub();
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default MyClubFrame;
\ No newline at end of file
diff --git a/2024-summer-FE-seminar/packages/web/src/features/ryan/frames/MyInfoFrame.tsx b/2024-summer-FE-seminar/packages/web/src/features/ryan/frames/MyInfoFrame.tsx
new file mode 100644
index 0000000..5e689ff
--- /dev/null
+++ b/2024-summer-FE-seminar/packages/web/src/features/ryan/frames/MyInfoFrame.tsx
@@ -0,0 +1,56 @@
+import { useEffect, useState } from "react";
+
+
+import AsyncBoundary from "@sparcs-clubs/web/common/components/AsyncBoundary";
+import Button from "@sparcs-clubs/web/common/components/Button";
+import Card from "@sparcs-clubs/web/common/components/Card";
+import FoldableSectionTitle from "@sparcs-clubs/web/common/components/FoldableSectionTitle";
+import PhoneInput from "@sparcs-clubs/web/common/components/Forms/PhoneInput";
+
+import { useGetUserProfile } from "../services/userProfile";
+
+
+const MyInfoFrame: React.FC = () => {
+ const {
+ data: userProfile,
+ isLoading,
+ isError,
+ } = useGetUserProfile();
+
+ const savedNumber = userProfile?.phoneNumber;
+
+ const [phoneNumber, setPhoneNumber] = useState(savedNumber ?? "");
+ const [phoneInput, setPhoneInput] = useState(phoneNumber);
+ const [phoneError, setPhoneError] = useState(false);
+
+ useEffect(() => {
+ if (userProfile) {
+ setPhoneNumber(savedNumber ?? "");
+ }
+ }, [userProfile, savedNumber]);
+
+
+ const buttonDisabled = phoneNumber === phoneInput || phoneError;
+
+ return
+
+
+
+
+
+
+
+}
+
+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)
});