From 6ea6e2da2704f744ec7cd4064e2f1633bf55ca2e Mon Sep 17 00:00:00 2001 From: saseungmin Date: Tue, 30 Jul 2024 17:03:47 +0900 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=EC=A7=80=EC=9B=90=EC=9E=90=20?= =?UTF-8?q?=EC=88=98=20=EC=B9=B4=EB=93=9C=20=EC=84=B9=EC=85=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .lintstagedrc.json | 3 -- apps/admin/src/actions/count.ts | 54 +++++++++++++------ apps/admin/src/app/page.tsx | 30 +---------- .../app/total-count-status/page.module.scss | 5 ++ .../admin/src/app/total-count-status/page.tsx | 35 ++++++++++++ apps/admin/src/components/Navigator/index.tsx | 13 +++++ .../TotalCountStatusForm/index.module.scss | 23 ++++++++ .../components/TotalCountStatusForm/index.tsx | 47 ++++++++++++++++ apps/web/src/app/jobs/template.tsx | 3 +- apps/web/src/app/organizers/page.tsx | 3 +- apps/web/src/app/projects/page.tsx | 3 +- apps/web/src/app/reviews/template.tsx | 3 +- .../SectionTitle/SectionTitle.stories.tsx | 2 +- .../src/components/atoms/Tag/Tag.stories.tsx | 2 +- .../src/components/pages/AboutPage/index.tsx | 3 +- .../components/pages/CulturePage/index.tsx | 3 +- packages/eslint-config/index.js | 7 +++ packages/ui/.eslintrc.js | 1 - packages/ui/@types/jest.d.ts | 3 ++ .../atoms/AccordionItem/index.test.tsx | 4 +- .../atoms/PageTitle/PageTitle.stories.tsx | 27 ++++++++++ .../atoms/PageTitle/index.module.scss | 0 .../components/atoms/PageTitle/index.test.tsx | 19 +++++++ .../src/components/atoms/PageTitle/index.tsx | 0 packages/ui/src/components/index.ts | 1 + .../molecules/Button/index.test.tsx | 4 +- packages/ui/tsconfig.jest.json | 6 --- packages/ui/tsconfig.json | 5 +- 28 files changed, 240 insertions(+), 69 deletions(-) create mode 100644 apps/admin/src/app/total-count-status/page.module.scss create mode 100644 apps/admin/src/app/total-count-status/page.tsx create mode 100644 apps/admin/src/components/Navigator/index.tsx create mode 100644 apps/admin/src/components/TotalCountStatusForm/index.module.scss create mode 100644 apps/admin/src/components/TotalCountStatusForm/index.tsx create mode 100644 packages/ui/@types/jest.d.ts create mode 100644 packages/ui/src/components/atoms/PageTitle/PageTitle.stories.tsx rename {apps/web => packages/ui}/src/components/atoms/PageTitle/index.module.scss (100%) create mode 100644 packages/ui/src/components/atoms/PageTitle/index.test.tsx rename {apps/web => packages/ui}/src/components/atoms/PageTitle/index.tsx (100%) delete mode 100644 packages/ui/tsconfig.jest.json diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 3ab5a440..f30e254e 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -13,8 +13,5 @@ ], "*.{js,jsx,ts,tsx}": [ "yarn test:coverage --findRelatedTests" - ], - "*.scss": [ - "yarn stylelint" ] } diff --git a/apps/admin/src/actions/count.ts b/apps/admin/src/actions/count.ts index db3236aa..99f31bc6 100644 --- a/apps/admin/src/actions/count.ts +++ b/apps/admin/src/actions/count.ts @@ -1,25 +1,47 @@ 'use server'; +import { revalidatePath } from 'next/cache'; + import { put } from '@vercel/blob'; +type TotalCountStatusStateType = { + message: string; + messageType?: 'error' | 'success'; +}; + // eslint-disable-next-line import/prefer-default-export -export const totalCountStatusAction = async (formData: FormData) => { - const requestForm = { - cumulativeApplicants: formData.get('cumulativeApplicants'), - dropouts: formData.get('dropouts'), - totalParticipants: formData.get('totalParticipants'), - totalProjects: formData.get('totalProjects'), - }; +export async function totalCountStatusAction( + _: TotalCountStatusStateType | null, + formData: FormData, +): Promise { + try { + const requestForm = { + cumulativeApplicants: formData.get('cumulativeApplicants'), + dropouts: formData.get('dropouts'), + totalParticipants: formData.get('totalParticipants'), + totalProjects: formData.get('totalProjects'), + }; - const jsonString = JSON.stringify(requestForm); + const jsonString = JSON.stringify(requestForm); - const requestBlob = new Blob([jsonString], { type: 'application/json' }); + const requestBlob = new Blob([jsonString], { type: 'application/json' }); - const blob = await put('total_count_status.json', requestBlob, { - access: 'public', - token: process.env.DND_ACADEMY_V2_BLOB_READ_WRITE_TOKEN, - addRandomSuffix: false, - }); + await put('total_count_status.json', requestBlob, { + access: 'public', + token: process.env.DND_ACADEMY_V2_BLOB_READ_WRITE_TOKEN, + addRandomSuffix: false, + }); - console.log(blob); -}; + revalidatePath('/total-count-status'); + + return { + message: '수정사항이 반영되었습니다. 캐시 적용으로 실제 적용까지는 최대 5분정도 소요됩니다.', + messageType: 'success', + }; + } catch (error) { + return { + message: '수정사항이 반영되지 않았습니다. 잠시 후 다시 시도해주세요.', + messageType: 'error', + }; + } +} diff --git a/apps/admin/src/app/page.tsx b/apps/admin/src/app/page.tsx index dfeeb52a..df19a0e6 100644 --- a/apps/admin/src/app/page.tsx +++ b/apps/admin/src/app/page.tsx @@ -1,11 +1,8 @@ import { redirect } from 'next/navigation'; -import { api, type TotalCountStatus } from '@dnd-academy/core'; -import { Button, CounterCard } from '@dnd-academy/ui'; - -import { totalCountStatusAction } from '@/actions/count'; import { auth } from '@/auth'; import SignOut from '@/components/auth/SignOut'; +import Navigator from '@/components/Navigator'; async function MainPage() { const session = await auth(); @@ -14,34 +11,11 @@ async function MainPage() { redirect('/login'); } - const { - cumulativeApplicants, dropouts, totalParticipants, totalProjects, - } = await api({ - url: '/total_count_status.json', - method: 'GET', - }); - return ( <>

DND - AdminPage

- -

지원자 수

-
-
- - - - -
-
- - - - - -
-
+ ); } diff --git a/apps/admin/src/app/total-count-status/page.module.scss b/apps/admin/src/app/total-count-status/page.module.scss new file mode 100644 index 00000000..5b4ac133 --- /dev/null +++ b/apps/admin/src/app/total-count-status/page.module.scss @@ -0,0 +1,5 @@ +.counterCardWrapper { + display: flex; + flex-direction: row; + gap: 10px; +} diff --git a/apps/admin/src/app/total-count-status/page.tsx b/apps/admin/src/app/total-count-status/page.tsx new file mode 100644 index 00000000..f39c7245 --- /dev/null +++ b/apps/admin/src/app/total-count-status/page.tsx @@ -0,0 +1,35 @@ +import { api, type TotalCountStatus } from '@dnd-academy/core'; +import { CounterCard, PageTitle } from '@dnd-academy/ui'; + +import TotalCountStatusForm from '@/components/TotalCountStatusForm'; + +import styles from './page.module.scss'; + +async function page() { + const totalCountStatus = await api({ + url: '/total_count_status.json', + method: 'GET', + }); + + const { + cumulativeApplicants, dropouts, totalParticipants, totalProjects, + } = totalCountStatus; + + return ( + <> + +
+ + + + +
+ + + ); +} + +export default page; diff --git a/apps/admin/src/components/Navigator/index.tsx b/apps/admin/src/components/Navigator/index.tsx new file mode 100644 index 00000000..0474ee3f --- /dev/null +++ b/apps/admin/src/components/Navigator/index.tsx @@ -0,0 +1,13 @@ +function Navigator() { + return ( + + ); +} + +export default Navigator; diff --git a/apps/admin/src/components/TotalCountStatusForm/index.module.scss b/apps/admin/src/components/TotalCountStatusForm/index.module.scss new file mode 100644 index 00000000..4e1adef4 --- /dev/null +++ b/apps/admin/src/components/TotalCountStatusForm/index.module.scss @@ -0,0 +1,23 @@ +.totalCountStatusFormWrapper { + display: flex; + flex-direction: column; + gap: 20px; + margin-top: 20px; + + .totalCountStatusForm { + display: flex; + flex-direction: row; + } + + .message { + @include text('body1'); + + &.error { + color: color('tertiary'); + } + + &.success { + color: color('success'); + } + } +} diff --git a/apps/admin/src/components/TotalCountStatusForm/index.tsx b/apps/admin/src/components/TotalCountStatusForm/index.tsx new file mode 100644 index 00000000..f01e612f --- /dev/null +++ b/apps/admin/src/components/TotalCountStatusForm/index.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useFormState, useFormStatus } from 'react-dom'; + +import { type TotalCountStatus } from '@dnd-academy/core'; +import { Button } from '@dnd-academy/ui'; +import clsx from 'clsx'; + +import { totalCountStatusAction } from '@/actions/count'; + +import styles from './index.module.scss'; + +type Props = { + initialTotalCountStatus: TotalCountStatus; +}; + +function TotalCountStatusForm({ initialTotalCountStatus }: Props) { + const { pending } = useFormStatus(); + const [state, formAction] = useFormState(totalCountStatusAction, null); + + const { + cumulativeApplicants, dropouts, totalParticipants, totalProjects, + } = initialTotalCountStatus; + + return ( +
+
+ + + + + +
+ {state?.message && ( +
+ {state.message} +
+ )} +
+ ); +} + +export default TotalCountStatusForm; diff --git a/apps/web/src/app/jobs/template.tsx b/apps/web/src/app/jobs/template.tsx index 6262c994..99b89edc 100644 --- a/apps/web/src/app/jobs/template.tsx +++ b/apps/web/src/app/jobs/template.tsx @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; -import PageTitle from '@/components/atoms/PageTitle'; +import { PageTitle } from '@dnd-academy/ui'; + import ShareAlarmSection from '@/components/organisms/ShareAlarmSection'; type Props = { diff --git a/apps/web/src/app/organizers/page.tsx b/apps/web/src/app/organizers/page.tsx index d521b632..270c691c 100644 --- a/apps/web/src/app/organizers/page.tsx +++ b/apps/web/src/app/organizers/page.tsx @@ -1,4 +1,5 @@ -import PageTitle from '@/components/atoms/PageTitle'; +import { PageTitle } from '@dnd-academy/ui'; + import ShareAlarmSection from '@/components/organisms/ShareAlarmSection'; import OrganizersPage from '@/components/pages/OrganizersPage'; import { getOrganizers } from '@/lib/apis/organizer'; diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index a806f71d..05dc376d 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -1,4 +1,5 @@ -import PageTitle from '@/components/atoms/PageTitle'; +import { PageTitle } from '@dnd-academy/ui'; + import ShareAlarmSection from '@/components/organisms/ShareAlarmSection'; import ProjectsPage from '@/components/pages/ProjectsPage'; import { getProjects } from '@/lib/apis/project'; diff --git a/apps/web/src/app/reviews/template.tsx b/apps/web/src/app/reviews/template.tsx index 92a1a35b..85cefce6 100644 --- a/apps/web/src/app/reviews/template.tsx +++ b/apps/web/src/app/reviews/template.tsx @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; -import PageTitle from '@/components/atoms/PageTitle'; +import { PageTitle } from '@dnd-academy/ui'; + import ShareAlarmSection from '@/components/organisms/ShareAlarmSection'; type Props = { diff --git a/apps/web/src/components/atoms/SectionTitle/SectionTitle.stories.tsx b/apps/web/src/components/atoms/SectionTitle/SectionTitle.stories.tsx index 532eed3d..b826cc63 100644 --- a/apps/web/src/components/atoms/SectionTitle/SectionTitle.stories.tsx +++ b/apps/web/src/components/atoms/SectionTitle/SectionTitle.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import SectionTitle from '.'; const meta = { - title: 'Components/SectionTitle', + title: 'atoms/SectionTitle', component: SectionTitle, parameters: { layout: 'centered', diff --git a/apps/web/src/components/atoms/Tag/Tag.stories.tsx b/apps/web/src/components/atoms/Tag/Tag.stories.tsx index 6ac61f07..bd13ad69 100644 --- a/apps/web/src/components/atoms/Tag/Tag.stories.tsx +++ b/apps/web/src/components/atoms/Tag/Tag.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import Tag from '.'; const meta = { - title: 'Components/Tag', + title: 'atoms/Tag', component: Tag, parameters: { layout: 'centered', diff --git a/apps/web/src/components/pages/AboutPage/index.tsx b/apps/web/src/components/pages/AboutPage/index.tsx index 17cf16f1..8e73ea20 100644 --- a/apps/web/src/components/pages/AboutPage/index.tsx +++ b/apps/web/src/components/pages/AboutPage/index.tsx @@ -2,7 +2,8 @@ import Marquee from 'react-fast-marquee'; import Image from 'next/image'; -import PageTitle from '@/components/atoms/PageTitle'; +import { PageTitle } from '@dnd-academy/ui'; + import SectionTitle from '@/components/atoms/SectionTitle'; import ImageCard from '@/components/molecules/ImageCard'; import CounterCardSection from '@/components/organisms/CounterCardSection'; diff --git a/apps/web/src/components/pages/CulturePage/index.tsx b/apps/web/src/components/pages/CulturePage/index.tsx index d082e724..0ca7c5b4 100644 --- a/apps/web/src/components/pages/CulturePage/index.tsx +++ b/apps/web/src/components/pages/CulturePage/index.tsx @@ -1,8 +1,7 @@ import Image from 'next/image'; -import { Button } from '@dnd-academy/ui'; +import { Button, PageTitle } from '@dnd-academy/ui'; -import PageTitle from '@/components/atoms/PageTitle'; import { LinkIcon } from '@/lib/assets/icons'; import styles from './index.module.scss'; diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index 18d2792c..c83a7dd4 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -62,6 +62,13 @@ module.exports = { 'react-hooks/rules-of-hooks': 'off', '@typescript-eslint/no-explicit-any': 'off', }, + settings: { + jest: { + globalAliases: { + describe: ['context'], + }, + }, + }, }, ], rules: { diff --git a/packages/ui/.eslintrc.js b/packages/ui/.eslintrc.js index 6ca0c011..6abfde59 100644 --- a/packages/ui/.eslintrc.js +++ b/packages/ui/.eslintrc.js @@ -16,7 +16,6 @@ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { project: [ - './tsconfig.jest.json', './tsconfig.json', ], tsconfigRootDir: __dirname, diff --git a/packages/ui/@types/jest.d.ts b/packages/ui/@types/jest.d.ts new file mode 100644 index 00000000..0b452d3c --- /dev/null +++ b/packages/ui/@types/jest.d.ts @@ -0,0 +1,3 @@ +/// + +declare let context: jest.Describe; diff --git a/packages/ui/src/components/atoms/AccordionItem/index.test.tsx b/packages/ui/src/components/atoms/AccordionItem/index.test.tsx index bbe08818..05c4fff3 100644 --- a/packages/ui/src/components/atoms/AccordionItem/index.test.tsx +++ b/packages/ui/src/components/atoms/AccordionItem/index.test.tsx @@ -25,7 +25,7 @@ describe('AccordionItem', () => { )); - describe('activeIndex와 currentIndex가 같은 경우', () => { + context('activeIndex와 currentIndex가 같은 경우', () => { const params = { activeIndex: 0, currentIndex: 0 }; it('자식 컴포넌트가 보여야만 한다', () => { @@ -45,7 +45,7 @@ describe('AccordionItem', () => { }); }); - describe('activeIndex와 currentIndex가 다른 경우', () => { + context('activeIndex와 currentIndex가 다른 경우', () => { const params = { activeIndex: 1, currentIndex: 0 }; it('자식 컴포넌트가 보이지 않아야만 한다', () => { diff --git a/packages/ui/src/components/atoms/PageTitle/PageTitle.stories.tsx b/packages/ui/src/components/atoms/PageTitle/PageTitle.stories.tsx new file mode 100644 index 00000000..31952910 --- /dev/null +++ b/packages/ui/src/components/atoms/PageTitle/PageTitle.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import PageTitle from '.'; + +const meta = { + title: 'atoms/PageTitle', + component: PageTitle, + parameters: { + layout: 'centered', + }, + args: { + title: 'title', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const HasSubtitle: Story = { + args: { + subTitle: 'subTitle', + }, +}; diff --git a/apps/web/src/components/atoms/PageTitle/index.module.scss b/packages/ui/src/components/atoms/PageTitle/index.module.scss similarity index 100% rename from apps/web/src/components/atoms/PageTitle/index.module.scss rename to packages/ui/src/components/atoms/PageTitle/index.module.scss diff --git a/packages/ui/src/components/atoms/PageTitle/index.test.tsx b/packages/ui/src/components/atoms/PageTitle/index.test.tsx new file mode 100644 index 00000000..1f3b238e --- /dev/null +++ b/packages/ui/src/components/atoms/PageTitle/index.test.tsx @@ -0,0 +1,19 @@ +import { render } from '@testing-library/react'; + +import PageTitle from '.'; + +describe('PageTitle', () => { + const title = 'title'; + const subtitle = 'subTitle'; + + const renderPageTitle = () => render(( + + )); + + it('title과 subTitle의 정보가 이 보여야함 한다', () => { + const { container } = renderPageTitle(); + + expect(container).toHaveTextContent(title); + expect(container).toHaveTextContent(subtitle); + }); +}); diff --git a/apps/web/src/components/atoms/PageTitle/index.tsx b/packages/ui/src/components/atoms/PageTitle/index.tsx similarity index 100% rename from apps/web/src/components/atoms/PageTitle/index.tsx rename to packages/ui/src/components/atoms/PageTitle/index.tsx diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index c2210a42..d708d64b 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -1,5 +1,6 @@ export { default as AccordionItem } from './atoms/AccordionItem'; export { default as Badge } from './atoms/Badge'; +export { default as PageTitle } from './atoms/PageTitle'; export { default as SkillTag } from './atoms/SkillTag'; export type { ButtonProps, ButtonSize } from './molecules/Button'; export { default as Button } from './molecules/Button'; diff --git a/packages/ui/src/components/molecules/Button/index.test.tsx b/packages/ui/src/components/molecules/Button/index.test.tsx index 6c834730..058f0294 100644 --- a/packages/ui/src/components/molecules/Button/index.test.tsx +++ b/packages/ui/src/components/molecules/Button/index.test.tsx @@ -12,7 +12,7 @@ describe('Button', () => { )); describe('"href" 속성 유무에 따라 버튼 또는 링크가 나타난다', () => { - describe('버튼인 경우', () => { + context('버튼인 경우', () => { it('"href" 속성이 없어야만 한다', () => { renderButton(); @@ -20,7 +20,7 @@ describe('Button', () => { }); }); - describe('링크인 경우', () => { + context('링크인 경우', () => { const href = '/dnd/about'; it('href 속성이 존재해야만 한다', () => { diff --git a/packages/ui/tsconfig.jest.json b/packages/ui/tsconfig.jest.json deleted file mode 100644 index 1dcb7f1e..00000000 --- a/packages/ui/tsconfig.jest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": [ - "src/**/*", - ] -} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 4150e7a2..d64ced96 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -12,7 +12,7 @@ "paths": { "@/*": ["src/*"], }, - "types": ["jest", "vite/client", "vite-plugin-svgr/client"] + "types": ["vite/client", "vite-plugin-svgr/client"] }, "include": [ "src", @@ -21,7 +21,8 @@ "client.d.ts", "vite.config.mts", "../../jest.setup.ts", - "__mocks__" + "__mocks__", + "@types" ], "exclude": ["dist", "build", "node_modules", "**/*.test.(ts|tsx)", "**/*.stories.(ts|tsx)"] }