Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(admin): 지원자 수 카드 섹션 수정 페이지 및 업데이트 플로우 구현 #161

Merged
merged 1 commit into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .lintstagedrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,5 @@
],
"*.{js,jsx,ts,tsx}": [
"yarn test:coverage --findRelatedTests"
],
"*.scss": [
"yarn stylelint"
]
}
54 changes: 38 additions & 16 deletions apps/admin/src/actions/count.ts
Original file line number Diff line number Diff line change
@@ -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<TotalCountStatusStateType> {
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',
};
}
}
30 changes: 2 additions & 28 deletions apps/admin/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -14,34 +11,11 @@ async function MainPage() {
redirect('/login');
}

const {
cumulativeApplicants, dropouts, totalParticipants, totalProjects,
} = await api<TotalCountStatus>({
url: '/total_count_status.json',
method: 'GET',
});

return (
<>
<h1>DND - AdminPage</h1>
<SignOut />

<h2>지원자 수</h2>
<div>
<div>
<CounterCard count={cumulativeApplicants} title="누적 지원자 수" />
<CounterCard count={totalParticipants} title="총 참가자 수" />
<CounterCard count={totalProjects} title="총 프로젝트 수" suffix="개" />
<CounterCard count={dropouts} title="이탈자 수" color="primary" />
</div>
<form action={totalCountStatusAction}>
<input type="number" name="cumulativeApplicants" min={0} placeholder="누적 지원자 수" defaultValue={cumulativeApplicants} required />
<input type="number" name="totalParticipants" min={0} placeholder="총 참가자 수" defaultValue={totalParticipants} required />
<input type="number" name="totalProjects" min={0} placeholder="총 프로젝트 수" defaultValue={totalProjects} required />
<input type="number" name="dropouts" min={0} placeholder="이탈자 수" defaultValue={dropouts} required />
<Button type="submit">Submit</Button>
</form>
</div>
<Navigator />
</>
);
}
Expand Down
5 changes: 5 additions & 0 deletions apps/admin/src/app/total-count-status/page.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.counterCardWrapper {
display: flex;
flex-direction: row;
gap: 10px;
}
35 changes: 35 additions & 0 deletions apps/admin/src/app/total-count-status/page.tsx
Original file line number Diff line number Diff line change
@@ -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<TotalCountStatus>({
url: '/total_count_status.json',
method: 'GET',
});

const {
cumulativeApplicants, dropouts, totalParticipants, totalProjects,
} = totalCountStatus;

return (
<>
<PageTitle
title="지원자 수 카드 섹션"
subTitle="캐시 적용으로 실제 적용까지는 최대 5분정도 소요됩니다."
/>
<div className={styles.counterCardWrapper}>
<CounterCard count={cumulativeApplicants} title="누적 지원자 수" />
<CounterCard count={totalParticipants} title="총 참가자 수" />
<CounterCard count={totalProjects} title="총 프로젝트 수" suffix="개" />
<CounterCard count={dropouts} title="이탈자 수" color="primary" />
</div>
<TotalCountStatusForm initialTotalCountStatus={totalCountStatus} />
</>
);
}

export default page;
13 changes: 13 additions & 0 deletions apps/admin/src/components/Navigator/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
function Navigator() {
return (
<nav>
<ul>
<li>
<a href="/total-count-status">지원자 수 카드 섹션 정보 업데이트하기</a>
</li>
</ul>
</nav>
);
}

export default Navigator;
23 changes: 23 additions & 0 deletions apps/admin/src/components/TotalCountStatusForm/index.module.scss
Original file line number Diff line number Diff line change
@@ -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');
}
}
}
47 changes: 47 additions & 0 deletions apps/admin/src/components/TotalCountStatusForm/index.tsx
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react에서는 useFormState가 useActionState로 변경되었지만, next.js에서는 아직 미반영이라 에러가 남.

vercel/next.js#65673


const {
cumulativeApplicants, dropouts, totalParticipants, totalProjects,
} = initialTotalCountStatus;

return (
<div className={styles.totalCountStatusFormWrapper}>
<form action={formAction} className={styles.totalCountStatusForm}>
<input type="number" name="cumulativeApplicants" min={0} placeholder="누적 지원자 수" defaultValue={cumulativeApplicants} required />
<input type="number" name="totalParticipants" min={0} placeholder="총 참가자 수" defaultValue={totalParticipants} required />
<input type="number" name="totalProjects" min={0} placeholder="총 프로젝트 수" defaultValue={totalProjects} required />
<input type="number" name="dropouts" min={0} placeholder="이탈자 수" defaultValue={dropouts} required />
<Button type="submit" disabled={pending}>업데이트하기</Button>
</form>
{state?.message && (
<div className={clsx(
styles.message,
state.messageType && styles[state.messageType],
)}
>
{state.message}
</div>
)}
</div>
);
}

export default TotalCountStatusForm;
3 changes: 2 additions & 1 deletion apps/web/src/app/jobs/template.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/organizers/page.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/reviews/template.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/atoms/Tag/Tag.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/pages/AboutPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/components/pages/CulturePage/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
7 changes: 7 additions & 0 deletions packages/eslint-config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ module.exports = {
'react-hooks/rules-of-hooks': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
settings: {
jest: {
globalAliases: {
describe: ['context'],
},
},
},
},
],
rules: {
Expand Down
1 change: 0 additions & 1 deletion packages/ui/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: [
'./tsconfig.jest.json',
'./tsconfig.json',
],
tsconfigRootDir: __dirname,
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/@types/jest.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/// <reference types="jest" />

declare let context: jest.Describe;
4 changes: 2 additions & 2 deletions packages/ui/src/components/atoms/AccordionItem/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('AccordionItem', () => {
</AccordionItem>
));

describe('activeIndex와 currentIndex가 같은 경우', () => {
context('activeIndex와 currentIndex가 같은 경우', () => {
const params = { activeIndex: 0, currentIndex: 0 };

it('자식 컴포넌트가 보여야만 한다', () => {
Expand All @@ -45,7 +45,7 @@ describe('AccordionItem', () => {
});
});

describe('activeIndex와 currentIndex가 다른 경우', () => {
context('activeIndex와 currentIndex가 다른 경우', () => {
const params = { activeIndex: 1, currentIndex: 0 };

it('자식 컴포넌트가 보이지 않아야만 한다', () => {
Expand Down
27 changes: 27 additions & 0 deletions packages/ui/src/components/atoms/PageTitle/PageTitle.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof PageTitle>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const HasSubtitle: Story = {
args: {
subTitle: 'subTitle',
},
};
Loading