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(community): 공지사항 페이지 #37

Merged
merged 33 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a38282f
chore: init lucide-react icon
SunwooJaeho Nov 28, 2024
b4e3ee1
feat: 공지사항 페이지
SunwooJaeho Nov 28, 2024
43ec47c
fix: 마지막 페이지인 상태로 검색 할 경우 리스트가 렌더링 되지 않는 현상
SunwooJaeho Nov 28, 2024
bc2200d
refactor: icon size 조정
SunwooJaeho Nov 28, 2024
f1bb7c0
refactor: icon size 조정
SunwooJaeho Nov 28, 2024
9f7467c
refactor: page size 조정
SunwooJaeho Nov 28, 2024
be38b71
chore: 불필요 주석 삭제
SunwooJaeho Nov 28, 2024
d6fb7a4
feat: hover 추가
SunwooJaeho Nov 28, 2024
a7568f6
refactor: 모바일 화면에서 title 위치 조정
SunwooJaeho Nov 28, 2024
6c6d5dd
feat: 페이지 전환 시 최상단으로 스크롤 이동
SunwooJaeho Nov 28, 2024
b711674
refactor: 쿼리 파라미터 적용
SunwooJaeho Nov 29, 2024
2925d61
refactor: type 이름 변경
SunwooJaeho Nov 29, 2024
ddfe8b4
chore: 주석 수정
SunwooJaeho Nov 29, 2024
8097869
refactor: pagination 분리
SunwooJaeho Nov 29, 2024
defb554
refactor: pagination props에 currentPage 추가
SunwooJaeho Nov 29, 2024
33342de
refactor: 불필요 공백 삭제 및 Link url 수정
SunwooJaeho Dec 1, 2024
0fa8ab5
fix: 새로고침 시 listStart가 초기화되는 현상
SunwooJaeho Dec 1, 2024
6b44468
refactor: 변수 선언 방식 변경
SunwooJaeho Dec 1, 2024
af89e6a
feat: input 컴포넌트 border 속성 추가
SunwooJaeho Dec 1, 2024
cc32c1b
refactor: ds input 컴포넌트로 변경
SunwooJaeho Dec 1, 2024
eeff403
refactor: 조건문 범위 설정
SunwooJaeho Dec 1, 2024
d5976e7
refactor: input 컴포넌트 variant 적용(primary/ghost)
SunwooJaeho Dec 1, 2024
36d84b7
refactor: type 수정
SunwooJaeho Dec 1, 2024
f63df06
refactor: boardList 토큰 적용
SunwooJaeho Dec 1, 2024
b8c505d
refactor: pagination response 타입 추가
SunwooJaeho Dec 1, 2024
aca9b2d
feat: size 토큰 추가
SunwooJaeho Dec 1, 2024
092b8e3
refactor: 미사용 타입 삭제
SunwooJaeho Dec 1, 2024
7eb72ed
refactor: size 토큰 삭제
SunwooJaeho Dec 2, 2024
0e699c1
refactor: pagination response type 수정
SunwooJaeho Dec 2, 2024
fbd5fe7
refactor: remote await 삭제
SunwooJaeho Dec 2, 2024
5bf115a
refactor: board 타입 정리
SunwooJaeho Dec 2, 2024
8565933
refactor: 파라미터 키 상수화
SunwooJaeho Dec 2, 2024
079cb3e
refactor: 파라미터 키 변경
SunwooJaeho Dec 2, 2024
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
604 changes: 604 additions & 0 deletions apps/community/src/app/api/mock/board/data.ts

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions apps/community/src/app/api/mock/board/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { board } from './data';

export function GET(request: Request) {
const url = new URL(request.url);
const page = Number(url.searchParams.get('page')) || 0;
const size = Number(url.searchParams.get('size')) || 10;
const keyword = url.searchParams.get('keyword') || '';
const category =
url.searchParams.get('category')?.toLowerCase().replace(/\s+/g, '') || '';

const filteredBoards = board.filter((board) => {
const matchesKeyword = board.title
.toLowerCase()
.replace(/\s+/g, '')
.includes(keyword);
const matchesCategory = category ? board.category === category : true;
return matchesKeyword && matchesCategory;
});

const totalElements = filteredBoards.length;
const totalPage = Math.ceil(totalElements / size);
const pagedBoards = filteredBoards.slice(page * size, (page + 1) * size);

const pageable = {
page,
size,
totalPage,
totalElements,
isEnd: page >= totalPage - 1,
};

return Response.json({
data: {
contents: pagedBoards,
pagable: pageable,
},
});
}
11 changes: 11 additions & 0 deletions apps/community/src/app/board/notice/page.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { themeVars } from '@aics-client/design-system/styles';
import { style } from '@vanilla-extract/css';

const boardWrapper = style({
display: themeVars.display.flex,
flexDirection: themeVars.flexDirection.column,
alignItems: themeVars.alignItems.center,
gap: '2rem',
});

export { boardWrapper };
42 changes: 42 additions & 0 deletions apps/community/src/app/board/notice/page.tsx
SunwooJaeho marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { PageHeader } from '~/components/page-header';

import { BoardList } from '~/components/board/board-list';
import { SearchBar } from '~/components/board/search-bar';

import * as styles from '~/app/board/notice/page.css';
import { Pagination } from '~/components/board/pagination';
import { getBoards } from './remote';

export const dynamic = 'force-dynamic';

export default async function BoardPage(props: {
searchParams?: Promise<{
category?: string;
page?: string;
keyword?: string;
}>;
}) {
const searchParams = await props.searchParams;
const currentPage = Number(searchParams?.page) || 0;
const keyword = searchParams?.keyword || '';

const { data } = await getBoards(currentPage, 10, keyword, '공지사항');

return (
<section>
<PageHeader
title="공지사항"
description="학부와 관련된 중요한 공지사항을 안내해드려요."
/>
<section className={styles.boardWrapper}>
<SearchBar placeholder="검색어를 입력하세요" />
<BoardList data={data.contents} />
<Pagination
totalPage={data.pagable.totalPage}
pageCount={5}
currentPage={currentPage}
/>
</section>
</section>
);
}
25 changes: 25 additions & 0 deletions apps/community/src/app/board/notice/remote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { http } from '~/utils/http';

import { MOCK_END_POINT } from '~/constants/api';

import type { PaginationResponse } from '~/types/api';

async function getBoards(
SunwooJaeho marked this conversation as resolved.
Show resolved Hide resolved
page: number,
size: number,
keyword: string,
category: string,
) {
const params = new URLSearchParams({
page: page.toString(),
size: size.toString(),
keyword: keyword,
category: category,
});

const url = `${MOCK_END_POINT.BOARD}?${params.toString()}`;
SunwooJaeho marked this conversation as resolved.
Show resolved Hide resolved

return await http.get<PaginationResponse>(url);
SunwooJaeho marked this conversation as resolved.
Show resolved Hide resolved
}

export { getBoards };
91 changes: 91 additions & 0 deletions apps/community/src/components/board/board-list.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { screen, themeVars } from '@aics-client/design-system/styles';
import { globalStyle, style } from '@vanilla-extract/css';

const boardListWrapper = style({
display: themeVars.display.flex,
flexDirection: themeVars.flexDirection.column,
width: themeVars.width.full,
borderTop: '1px solid',
borderColor: themeVars.color.gray300,
});

const row = style({
display: themeVars.display.flex,
alignItems: themeVars.alignItems.center,
minWidth: themeVars.width.full,
maxHeight: '4rem',
gap: themeVars.spacing.md,
paddingTop: '1rem',
paddingBottom: '1rem',
borderBottom: '1px solid',
borderColor: themeVars.color.gray300,
textOverflow: 'ellipsis',
selectors: {
'&:hover': {
backgroundColor: themeVars.color.gray100,
},
},

...screen.xl({
minHeight: '3.5rem',
}),
});

globalStyle(`${row} > * + *`, {
textAlign: 'center',
});

const pin = style({
display: themeVars.display.flex,
justifyContent: themeVars.justifyContent.center,
width: '5%',
});

const rowTitle = style({
display: themeVars.display.flex,
justifyContent: themeVars.justifyContent.center,
alignItems: themeVars.alignItems.center,
width: '90%',
gap: themeVars.spacing.sm,
fontWeight: themeVars.fontWeight.semibold,

...screen.md({
width: '60%',
justifyContent: themeVars.justifyContent.start,
}),
});

const information = style({
width: '0%',
visibility: 'hidden',

...screen.md({
display: themeVars.display.flex,
justifyContent: themeVars.justifyContent.end,
alignItems: themeVars.alignItems.center,
width: '35%',
visibility: 'visible',
fontSize: themeVars.fontSize.xs,
gap: '0.75rem',
paddingRight: '1.5rem',
}),
});

const view = style({
display: themeVars.display.flex,
justifyContent: themeVars.justifyContent.center,
alignItems: themeVars.alignItems.center,
gap: themeVars.spacing.xs,
});

const author = style({
width: '0%',
visibility: 'hidden',

...screen.xl({
visibility: 'visible',
width: 'auto',
}),
});
SunwooJaeho marked this conversation as resolved.
Show resolved Hide resolved

export { boardListWrapper, row, pin, rowTitle, information, view, author };
55 changes: 55 additions & 0 deletions apps/community/src/components/board/board-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Link from 'next/link';

import { Eye, Paperclip, Pin } from '@aics-client/design-system/icons';

import { MOCK_END_POINT } from '~/constants/api';

import type { Board } from '~/types/board';

import * as styles from '~/components/board/board-list.css';

async function BoardList({ data }: { data: Board[] }) {
Copy link
Member

@gwansikk gwansikk Nov 29, 2024

Choose a reason for hiding this comment

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

해당 컴포넌트는 추상화 레벨, 결합도가 높다는 것이 걱정이네요. 현재 상태에서는 DS의 리스트에서 사용하기 어렵다고 판단되어 이와 같이 구현한 것으로 보여지는데요. 일단 넘어가고 나중에 DS 리스트를 활용하는 것이 좋아보여요.

다른 분들 좋은 아이디어가 있다면 피드백주세요

return (
<>
{data.length > 0 ? (
<ul className={styles.boardListWrapper}>
{data.map((row) => (
<Row key={row.postId} data={row} />
))}
</ul>
) : (
<p>게시물이 존재하지 않습니다.</p>
)}
</>
);
}

function Row({ data }: { data: Board }) {
return (
<Link href={`${MOCK_END_POINT.BOARD_DETAIL}/${data.postId}`}>
<li className={styles.row}>
<div className={styles.pin}>
{data.isPinned ? (
<Pin fill="black" size={'1.25rem'} />
) : (
<span>{data.postId}</span>
)}
</div>
<div className={styles.rowTitle}>
<h2>{data.title}</h2>
{data.hasAttachment && <Paperclip color="grey" size={'1rem'} />}
</div>
<div className={styles.information}>
<div className={styles.view}>
<Eye size={'1rem'} />
<span>{data.views}</span>
</div>
<div className={styles.author}>{data.author}</div>
<div>{data.createAt}</div>
</div>
</li>
</Link>
);
}

export { BoardList };
35 changes: 35 additions & 0 deletions apps/community/src/components/board/pagination.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { themeVars } from '@aics-client/design-system/styles';
import { style } from '@vanilla-extract/css';

const controllerWrapper = style({
display: themeVars.display.flex,
justifyContent: themeVars.justifyContent.center,
alignItems: themeVars.alignItems.center,
minWidth: '30%',
gap: '0.75rem',
userSelect: 'none',
});

const buttonList = style({
display: themeVars.display.flex,
alignItems: themeVars.alignItems.center,
});

const pageButton = style({
width: '2rem',
height: '2rem',
borderRadius: themeVars.borderRadius.lg,
textAlign: 'center',
});

const active = style({
fontWeight: themeVars.fontWeight.semibold,
color: themeVars.color.white,
backgroundColor: themeVars.color.black,
});

const hidden = style({
visibility: 'hidden',
});

export { controllerWrapper, buttonList, pageButton, active, hidden };
76 changes: 76 additions & 0 deletions apps/community/src/components/board/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { Fragment, useMemo, useState } from 'react';

import { ChevronLeft, ChevronRight } from '@aics-client/design-system/icons';

import * as styles from '~/components/board/pagination.css';

interface Props {
totalPage: number; // 총 페이지 수
pageCount: number; // 보여줄 페이지 장 수
currentPage: number; // 현재 페이지
}

function Pagination({ totalPage, pageCount, currentPage }: Props) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const initialListStart = Math.floor(currentPage / pageCount) * pageCount + 1;
const [listStart, setListStart] = useState(initialListStart);

const noPrev = listStart === 1;
const noNext = listStart + pageCount - 1 >= totalPage;

const handleMovePage = (pageNum: number) => {
const newListStart =
Math.ceil(pageNum / pageCount) * pageCount - (pageCount - 1);

if (newListStart !== listStart) {
setListStart(newListStart > 0 ? newListStart : 1);
}

const params = new URLSearchParams(searchParams);
params.set('page', (pageNum - 1).toString());

router.push(`${pathname}?${params.toString()}`);
};

return (
<div className={styles.controllerWrapper}>
<button
type="button"
onClick={() => handleMovePage(listStart - 1)}
className={`${noPrev && styles.hidden}`}
>
<ChevronLeft />
</button>
{[...Array(pageCount)].map((_, i) => (
<Fragment key={`page-${listStart + i}`}>
{listStart + i <= totalPage && (
<button
type="button"
className={`${styles.pageButton} ${
currentPage + 1 === listStart + i && styles.active
}`}
onClick={() => handleMovePage(listStart + i)}
>
{listStart + i}
</button>
)}
</Fragment>
))}
<button
type="button"
onClick={() => handleMovePage(listStart + pageCount)}
className={`${noNext && styles.hidden}`}
>
<ChevronRight />
</button>
</div>
);
}

export { Pagination };
Loading