From 55b19a2f63ccfcb471e185977c87afbd3b172951 Mon Sep 17 00:00:00 2001 From: SeieunYoo <101736358+SeieunYoo@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:51:18 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=88=98=EA=B0=95=EC=8B=A0=EC=B2=AD=20api=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=20(#31)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디 신청 api 연결 및 응답 포맷팅 로직 추가 * feat: body 추가 * feat: 헤더 뺀 부분 * refactor: parseDate 상단에서 하도록 수정, padWitZero 유틸 추가 * feat: padWithZero export * feat: utils 패키지에 next 설치 * fix: fetcher 클래스 client, server 환경 분리 로직 추가 * chore: root 의존성 충돌 문제 해결 * chore: 불필요한 헤더 주입 로직 삭제 * chore: admin 헤더 관련 로직 삭제 * fix: 빌드 실패하는 문제 해결 * feat: async await 으로 변경, success 여부만 넘기는 것으로 * refactor: Table.Content 삭제, 핸들러 이름 변경 * feat: 에러 페이지 추가, header 제거 * feat: Table export 추가 및 빌드 스크립트 수정 * fix: 빌드 에러 수정 * feat: Table Content 부분 삭제 * fix: 구조분해할당으로 리팩토링 * fix: respose.ok 값 체크 * chore: 필요없는 주석 제거 * chore: notion link 로 열리게 수정 --------- Co-authored-by: ghdtjgus76 --- .github/workflows/build.yml | 4 +- apps/admin/apis/auth/dashboardApi.ts | 9 +- apps/admin/middleware.ts | 5 +- apps/client/apis/dashboardApi.ts | 8 -- apps/client/apis/studyApplyApi.ts | 31 ++++++ .../_components/HomeworkHistory.tsx | 12 ++- .../study-apply/_components/StudyItem.tsx | 102 ++++++++++++++++++ .../study-apply/_components/index.ts | 1 + .../app/(afterLogin)/study-apply/page.tsx | 41 ++----- apps/client/app/error.tsx | 6 ++ apps/client/constants/apiPath.ts | 2 + apps/client/constants/dayToKorean.ts | 9 ++ apps/client/constants/tags.ts | 1 + apps/client/types/dtos/apply-study.ts | 19 ++++ package.json | 3 +- .../ui/src/components/Table/Table.stories.tsx | 44 -------- packages/ui/src/components/Table/index.tsx | 32 +----- packages/ui/src/styles.css | 23 +--- packages/utils/package.json | 9 +- packages/utils/src/fetcher/index.ts | 27 ++++- packages/utils/src/formatDate.ts | 18 ++++ packages/utils/src/formatTime.ts | 22 ++++ packages/utils/src/index.ts | 2 + pnpm-lock.yaml | 6 +- 24 files changed, 275 insertions(+), 161 deletions(-) create mode 100644 apps/client/apis/studyApplyApi.ts create mode 100644 apps/client/app/(afterLogin)/study-apply/_components/StudyItem.tsx create mode 100644 apps/client/app/(afterLogin)/study-apply/_components/index.ts create mode 100644 apps/client/app/error.tsx create mode 100644 apps/client/constants/dayToKorean.ts create mode 100644 apps/client/types/dtos/apply-study.ts create mode 100644 packages/utils/src/formatDate.ts create mode 100644 packages/utils/src/formatTime.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c62b904f..79f73e92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: - main - dev pull_request: - types: [opened, ready_for_review, converted_to_draft] + types: [opened, ready_for_review, converted_to_draft, synchronize] branches: - main - dev @@ -34,7 +34,7 @@ jobs: run_install: false - name: Install dependencies - run: pnpm install + run: pnpm install --no-frozen-lockfile - name: Run build run: pnpm run build diff --git a/apps/admin/apis/auth/dashboardApi.ts b/apps/admin/apis/auth/dashboardApi.ts index f3277ebb..c3a12369 100644 --- a/apps/admin/apis/auth/dashboardApi.ts +++ b/apps/admin/apis/auth/dashboardApi.ts @@ -1,21 +1,14 @@ import { fetcher } from "@wow-class/utils"; import { apiPath } from "constants/apiPath"; import { tags } from "constants/tags"; -import { cookies } from "next/headers"; -import type { DashboardApiResponseDto } from "types/dto/auth"; +import type { DashboardApiResponseDto } from "types/dtos/auth"; export const dashboardApi = { getDashboardInfo: async () => { - const cookieStore = cookies(); - const accessToken = cookieStore.get("accessToken")?.value; - const response = await fetcher.get( apiPath.dashboard, { next: { tags: [tags.dashboard] }, - headers: { - Authorization: `Bearer ${accessToken}`, - }, } ); diff --git a/apps/admin/middleware.ts b/apps/admin/middleware.ts index 7ada0433..4e1e165a 100644 --- a/apps/admin/middleware.ts +++ b/apps/admin/middleware.ts @@ -25,11 +25,8 @@ const middleware = async (req: NextRequest) => { return NextResponse.redirect(new URL("/auth", url)); } - const response = NextResponse.next(); - response.headers.set("Authorization", `Bearer ${accessToken}`); - - return response; + return NextResponse.next(); }; export default middleware; diff --git a/apps/client/apis/dashboardApi.ts b/apps/client/apis/dashboardApi.ts index 445b1f2c..6650ad95 100644 --- a/apps/client/apis/dashboardApi.ts +++ b/apps/client/apis/dashboardApi.ts @@ -1,22 +1,14 @@ import { fetcher } from "@wow-class/utils"; import { apiPath } from "constants/apiPath"; import { tags } from "constants/tags"; -import { cookies } from "next/headers"; import type { DashboardApiResponseDto } from "types/dtos/auth"; export const dashboardApi = { getDashboardInfo: async () => { - const cookieStore = cookies(); - const accessToken = cookieStore.get("accessToken")?.value; - - // NOTE: middleware에서 호출하기 위해서 별도로 헤더 주입 const response = await fetcher.get( apiPath.dashboard, { next: { tags: [tags.dashboard] }, - headers: { - Authorization: `Bearer ${accessToken}`, - }, } ); diff --git a/apps/client/apis/studyApplyApi.ts b/apps/client/apis/studyApplyApi.ts new file mode 100644 index 00000000..44e64bf0 --- /dev/null +++ b/apps/client/apis/studyApplyApi.ts @@ -0,0 +1,31 @@ +import { fetcher } from "@wow-class/utils"; +import { apiPath } from "constants/apiPath"; +import { tags } from "constants/tags"; +import type { StudyListApiResponseDto } from "types/dtos/apply-study"; + +export const studyApplyApi = { + getStudyList: async () => { + const response = await fetcher.get( + apiPath.applyStudy, + { + next: { tags: [tags.studyApply] }, + cache: "force-cache", + } + ); + + return response.data; + }, + applyStudy: async (studyId: number) => { + const response = await fetcher.post( + `${apiPath.applyStudy}/${studyId}`, + null + ); + + return { success: response.ok }; + }, + cancelStudyApplication: async (studyId: number) => { + const response = await fetcher.delete(`${apiPath.applyStudy}/${studyId}`); + + return { success: response.ok }; + }, +}; diff --git a/apps/client/app/(afterLogin)/my-study/my-homework/_components/HomeworkHistory.tsx b/apps/client/app/(afterLogin)/my-study/my-homework/_components/HomeworkHistory.tsx index 4c426e42..42f2d45b 100644 --- a/apps/client/app/(afterLogin)/my-study/my-homework/_components/HomeworkHistory.tsx +++ b/apps/client/app/(afterLogin)/my-study/my-homework/_components/HomeworkHistory.tsx @@ -1,4 +1,4 @@ -import { styled } from "@styled-system/jsx"; +import { Flex, styled } from "@styled-system/jsx"; import { Space, Table, Text } from "@wow-class/ui"; import Button from "wowds-ui/Button"; import Tag from "wowds-ui/Tag"; @@ -22,10 +22,12 @@ export const HomeworkHistory = () => { 1주차 - + + (과제 제목) HTTP 통신 코드 작성하기 + + 종료 : 2024년 5월 23일 23:59 + + diff --git a/apps/client/app/(afterLogin)/study-apply/_components/StudyItem.tsx b/apps/client/app/(afterLogin)/study-apply/_components/StudyItem.tsx new file mode 100644 index 00000000..08613ba3 --- /dev/null +++ b/apps/client/app/(afterLogin)/study-apply/_components/StudyItem.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { css } from "@styled-system/css"; +import { Flex, styled } from "@styled-system/jsx"; +import { Table, Text } from "@wow-class/ui"; +import { parseISODate, splitTime } from "@wow-class/utils"; +import { studyApplyApi } from "apis/studyApplyApi"; +import { dayToKorean } from "constants/dayToKorean"; +import type { ComponentProps } from "react"; +import type { StudyListApiResponseDto } from "types/dtos/apply-study"; +import Button from "wowds-ui/Button"; +import Tag from "wowds-ui/Tag"; + +interface StudyItemProps { + study: StudyListApiResponseDto; +} + +const StudyItem = ({ study }: StudyItemProps) => { + const { + studyId, + title, + introduction, + notionLink, + mentorName, + studyType, + dayOfWeek, + startTime: startTimeString, + openingDate: openingDateString, + totalWeek, + } = study; + + const handleClickApplyButton = async () => { + const result = await studyApplyApi.applyStudy(studyId); + + if (!result.success) { + console.error("스터디 신청 실패"); + } else { + console.log("스터디 신청 성공"); + } + }; + + const handleClickCancelButton = async () => { + const result = await studyApplyApi.cancelStudyApplication(studyId); + + if (!result.success) { + console.error("스터디 신청 실패"); + } else { + console.log("스터디 취소 성공"); + } + }; + + const startTime = splitTime(startTimeString); + const openingDate = parseISODate(openingDateString); + const studyTime = `${dayToKorean[dayOfWeek.toUpperCase()]} ${startTime.hours}:${startTime.minutes} - ${ + Number(startTime.hours) + 1 + }:${startTime.minutes}`; + + return ( + + + + {title} + + {studyType} + + + + {`${introduction} -`} + + {notionLink} + + + + {mentorName} + {studyTime} + {totalWeek}주 코스 + + {`${openingDate.month}.${openingDate.day} 개강`} + + + + + +
+ ); +}; + +const textCellStyle = css({ + paddingX: "28px", +}); + +const sessionColors: Record["color"]> = { + "과제 스터디": "green", + "온라인 세션": "blue", + "오프라인 세션": "yellow", +}; + +export default StudyItem; diff --git a/apps/client/app/(afterLogin)/study-apply/_components/index.ts b/apps/client/app/(afterLogin)/study-apply/_components/index.ts new file mode 100644 index 00000000..941d5025 --- /dev/null +++ b/apps/client/app/(afterLogin)/study-apply/_components/index.ts @@ -0,0 +1 @@ +export { default as StudytItem } from "./StudyItem"; diff --git a/apps/client/app/(afterLogin)/study-apply/page.tsx b/apps/client/app/(afterLogin)/study-apply/page.tsx index 8030b8d2..c1ee6758 100644 --- a/apps/client/app/(afterLogin)/study-apply/page.tsx +++ b/apps/client/app/(afterLogin)/study-apply/page.tsx @@ -1,45 +1,22 @@ -import { css } from "@styled-system/css"; -import { styled } from "@styled-system/jsx"; -import { Space, Table, Text } from "@wow-class/ui"; -import Button from "wowds-ui/Button"; -import Tag from "wowds-ui/Tag"; +import { Space, Text } from "@wow-class/ui"; +import { studyApplyApi } from "apis/studyApplyApi"; + +import StudyItem from "./_components/StudyItem"; + +const StudyApplyPage = async () => { + const studyList = await studyApplyApi.getStudyList(); -const StudyApplyPage = () => { - const array = [0, 1, 2]; return ( <> 신청 가능한 스터디 - {array.map(() => ( - - - 신규 - - } - /> - 강가은 멘토 - 화 18:00-19:00 - 4주 코스 - 06.18 개강 - - - -
+ {studyList?.map((study) => ( + ))} ); }; export default StudyApplyPage; - -const textCellStyle = css({ - paddingX: "28px", -}); diff --git a/apps/client/app/error.tsx b/apps/client/app/error.tsx new file mode 100644 index 00000000..1344c8e2 --- /dev/null +++ b/apps/client/app/error.tsx @@ -0,0 +1,6 @@ +"use client"; +const ErrorPage = () => { + return
error
; +}; + +export default ErrorPage; diff --git a/apps/client/constants/apiPath.ts b/apps/client/constants/apiPath.ts index 54ac013f..29c0fa6f 100644 --- a/apps/client/constants/apiPath.ts +++ b/apps/client/constants/apiPath.ts @@ -1,3 +1,5 @@ export const enum apiPath { dashboard = "/onboarding/members/me/dashboard", + + applyStudy = "/studies/apply", } diff --git a/apps/client/constants/dayToKorean.ts b/apps/client/constants/dayToKorean.ts new file mode 100644 index 00000000..6cc427e8 --- /dev/null +++ b/apps/client/constants/dayToKorean.ts @@ -0,0 +1,9 @@ +export const dayToKorean: Record = { + MONDAY: "월", + TUESDAY: "화", + WEDNESDAY: "수", + THURSDAY: "목", + FRIDAY: "금", + SATURDAY: "토", + SUNDAY: "일", +}; diff --git a/apps/client/constants/tags.ts b/apps/client/constants/tags.ts index faca2ba3..5f36c2b8 100644 --- a/apps/client/constants/tags.ts +++ b/apps/client/constants/tags.ts @@ -1,3 +1,4 @@ export const enum tags { dashboard = "dashboard", + studyApply = "studyApply", } diff --git a/apps/client/types/dtos/apply-study.ts b/apps/client/types/dtos/apply-study.ts new file mode 100644 index 00000000..4c223cee --- /dev/null +++ b/apps/client/types/dtos/apply-study.ts @@ -0,0 +1,19 @@ +export interface StudyListApiResponseDto { + studyId: number; + title: string; + studyType: string; + notionLink: string; + introduction: string; + mentorName: string; + dayOfWeek: + | "MONDAY" + | "TUESDAY" + | "WEDNESDAY" + | "THURSDAY" + | "FRIDAY" + | "SATURDAY" + | "SUNDAY"; + startTime: string; + totalWeek: number; + openingDate: string; +} diff --git a/package.json b/package.json index 9eb5d569..8caa0e99 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "clsx": "^2.1.1", "wowds-icons": "^0.1.3", "wowds-tokens": "^0.1.1", - "wowds-ui": "^0.1.8", - "wowds-icons": "^0.1.2" + "wowds-ui": "^0.1.8" } } diff --git a/packages/ui/src/components/Table/Table.stories.tsx b/packages/ui/src/components/Table/Table.stories.tsx index 29aa1008..01148079 100644 --- a/packages/ui/src/components/Table/Table.stories.tsx +++ b/packages/ui/src/components/Table/Table.stories.tsx @@ -1,5 +1,4 @@ import type { Meta, StoryObj } from "@storybook/react"; -import Tag from "wowds-ui/Tag"; import Text from "../Text"; import Table from "."; @@ -31,46 +30,3 @@ export const Default: Story = { ), }, }; - -export const WithContent: Story = { - args: { - children: ( - <> - 기초 웹스터디 - - 온라인 세션 - - } - /> - 오른쪽 컨텐츠 - - ), - }, -}; - -export const WithoutSubText: Story = { - args: { - children: ( - <> - - 기초 웹스터디 - - 온라인 세션 - - } - /> - - - 오른쪽 컨텐츠 - - - ), - }, -}; diff --git a/packages/ui/src/components/Table/index.tsx b/packages/ui/src/components/Table/index.tsx index 3f38d9eb..20245341 100644 --- a/packages/ui/src/components/Table/index.tsx +++ b/packages/ui/src/components/Table/index.tsx @@ -1,13 +1,5 @@ import { Flex } from "@styled-system/jsx"; -import type { PropsWithChildren, ReactNode } from "react"; - -import Text from "../Text"; - -interface TableContentProps { - text: string; - subText?: string; - rightContent?: ReactNode; -} +import type { PropsWithChildren } from "react"; /** * @description Table 컴포넌트. 기본적으로 space-between로 정렬되어 있습니다. @@ -50,26 +42,4 @@ Table.Right = ({ children }: PropsWithChildren) => { ); }; -/** - * @description Table.Content 컴포넌트 - * @param {string} text - 텍스트 - * @param {string} subText - 서브 텍스트 - * @param {ReactNode} rightContent - 텍스트 영역의 오른쪽 컨텐츠 - */ -Table.Content = ({ text, subText, rightContent }: TableContentProps) => { - return ( - - - {text && {text}} - {rightContent} - - {subText && ( - - {subText} - - )} - - ); -}; - export default Table; diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index cb522b7f..aa255d96 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -239,8 +239,6 @@ progress { --colors-mono-950: #121212; --colors-white: #ffffff; --colors-black: #000000; - --spacing-xxs: 0.25rem; - --spacing-xs: 0.5rem; --border-widths-button: 1px; --colors-primary: #368ff7; --colors-success: #2a8642; @@ -400,18 +398,6 @@ progress { .h_80px:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { height: 80px; } -.gap_xxs:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - gap: var(--spacing-xxs); -} -.gap_xs:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - gap: var(--spacing-xs); -} -.c_sub:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - color: var(--colors-sub); -} -.c_blue:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - color: blue; -} .c_textBlack:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { color: var(--colors-text-black); } @@ -658,6 +644,9 @@ progress { .c_errorBackground:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { color: var(--colors-error-background); } +.c_sub:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-sub); +} .c_outline:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { color: var(--colors-outline); } @@ -793,9 +782,3 @@ progress { .jc_flex-end:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { justify-content: flex-end; } -.flex-d_column:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - flex-direction: column; -} -.jc_center:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - justify-content: center; -} diff --git a/packages/utils/package.json b/packages/utils/package.json index 4ba994ad..5410d84a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -10,11 +10,14 @@ }, "devDependencies": { "@types/jest": "^29.5.12", + "@wow-class/eslint-config": "workspace:*", "@wow-class/typescript-config": "workspace:*", "jest": "^29.7.0", "jest-fetch-mock": "^3.0.3", - "ts-jest": "^29.2.4", - "@wow-class/eslint-config": "workspace:*", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "ts-jest": "^29.2.4" + }, + "dependencies": { + "next": "^14.2.5" } } diff --git a/packages/utils/src/fetcher/index.ts b/packages/utils/src/fetcher/index.ts index df222dc9..c3194f67 100644 --- a/packages/utils/src/fetcher/index.ts +++ b/packages/utils/src/fetcher/index.ts @@ -87,9 +87,14 @@ class Fetcher { ): Promise> { options = await this.interceptRequest(options); + const fetchOptions: RequestInit = { + ...options, + credentials: "include", + }; + const fullUrl = this.baseUrl + url; - let response: ApiResponse = await fetch(fullUrl, options); + let response: ApiResponse = await fetch(fullUrl, fetchOptions); await this.handleError(response); @@ -157,6 +162,8 @@ class Fetcher { } } +const isClient = typeof window !== "undefined"; + const fetcher = new Fetcher({ baseUrl: process.env.NODE_ENV === "production" @@ -165,4 +172,22 @@ const fetcher = new Fetcher({ defaultHeaders: { "Content-Type": "application/json" }, }); +if (!isClient) { + fetcher.addRequestInterceptor(async (options) => { + const { cookies } = await import("next/headers"); + + const cookieStore = cookies(); + const accessToken = cookieStore.get("accessToken")?.value; + + if (accessToken) { + options.headers = { + ...options.headers, + Authorization: `Bearer ${accessToken}`, + }; + } + + return options; + }); +} + export default fetcher; diff --git a/packages/utils/src/formatDate.ts b/packages/utils/src/formatDate.ts new file mode 100644 index 00000000..b1fa32aa --- /dev/null +++ b/packages/utils/src/formatDate.ts @@ -0,0 +1,18 @@ +/** + * + * @description ISO Date 형식의 string 을 hours, minutes, seconds, year, month, day 로 파싱합니다. + * @example parseDate("2021-08-31T00:00:00.000Z") -> { year: 2021, month: 8, day: 31, hours: 0, minutes: 0, seconds: 0 } + */ + +export const parseISODate = (dateString: string) => { + const date = new Date(dateString); + + return { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate(), + hours: date.getHours(), + minutes: date.getMinutes(), + seconds: date.getSeconds(), + }; +}; diff --git a/packages/utils/src/formatTime.ts b/packages/utils/src/formatTime.ts new file mode 100644 index 00000000..b2202b81 --- /dev/null +++ b/packages/utils/src/formatTime.ts @@ -0,0 +1,22 @@ +/** + * @description 10보다 작은 숫자에 0을 붙여 반환합니다. + * @example padWithZero(2) -> "02" + */ + +export const padWithZero = (number: number) => { + return number.toString().padStart(2, "0"); +}; + +/** + * @description 시간을 받아 시간, 분, 초로 나누어 반환합니다. + * @example splitTime("12:30:00") -> { hours: "12", minutes: "30", seconds: "00" } + */ + +export const splitTime = (timeString: string) => { + const [hours, minutes, seconds] = timeString.split(":"); + return { + hours, + minutes, + seconds, + }; +}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 96abf254..05260e73 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1 +1,3 @@ export { default as fetcher } from "./fetcher"; +export * from "./formatDate"; +export * from "./formatTime"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a312618..479e80da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,7 +18,7 @@ importers: specifier: ^0.1.1 version: 0.1.3 wowds-ui: - specifier: ^0.1.13 + specifier: ^0.1.8 version: 0.1.13(next@14.2.5)(react-dom@18.3.1)(react@18.3.1) devDependencies: '@pandacss/dev': @@ -261,6 +261,10 @@ importers: version: 5.4.5 packages/utils: + dependencies: + next: + specifier: ^14.2.5 + version: 14.2.5(@babel/core@7.25.2)(react-dom@18.3.1)(react@18.3.1) devDependencies: '@types/jest': specifier: ^29.5.12