-
Notifications
You must be signed in to change notification settings - Fork 0
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
[Dony] Table 컴포넌트 구현 #7
Changes from 3 commits
b75236f
ceb007c
d7001fe
07a9ad2
b90863f
fd8f58f
c6f4283
a7a0a32
8dfd8ba
5734bd9
bf52d00
6a8a858
b2e420a
91cf1b0
b7f6aae
54cddc5
025fea6
66d6c78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { ColumnTable, Text, FlexBox } from '@cos-ui/react'; | ||
|
||
const noticeList = [ | ||
{ | ||
id: 1, | ||
title: '스터디 가입전 해당 공지사항을 숙지해주세요.', | ||
writer: '파크', | ||
date: '2022.10.22', | ||
view: 101, | ||
}, | ||
{ | ||
id: 2, | ||
title: '스터디 전체 공지 드립니다.', | ||
writer: '도니', | ||
date: '2022.09.20', | ||
view: 110, | ||
}, | ||
{ | ||
id: 3, | ||
title: '스터디 전체 공지 드립니다.', | ||
writer: '햄디', | ||
date: '2022.09.20', | ||
view: 9, | ||
}, | ||
{ | ||
id: 4, | ||
title: '스터디 전체 공지 드립니다.', | ||
writer: '제이미', | ||
date: '2022.09.22', | ||
view: 9, | ||
}, | ||
]; | ||
|
||
const sortValues = { | ||
writer: noticeList.map(({ writer }) => writer), | ||
date: noticeList.map(({ date }) => { | ||
const numberDate = Number(date.split('.').join('')); | ||
return numberDate.toString().length < 8 ? numberDate * 10 : numberDate; | ||
}), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금 date 형태가 "YYYY.MM.DD" 인데, 표준으로 쓰이는 Date 의 형태를 쓰는게 어떤가요? (저희 서버에서는 new Date().getTime() 한 값과 동일한 형태로 줍니다) 사용자가 원하는 형태로 쓸 수 있다는 유연함을 줄 수있는 방식이라고 생각해서 방식 자체는 좋다고 생각합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getTime으로 주시는거면 그 값 그대로 사용해서 비교하면 되겠네용 ^0^ |
||
view: noticeList.map(({ view }) => Number(view)), | ||
}; | ||
|
||
const SortIcon = () => ( | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="17" | ||
height="14" | ||
viewBox="0 0 17 14" | ||
> | ||
<path | ||
id="다각형_3" | ||
data-name="다각형 3" | ||
d="M7.645,1.408a1,1,0,0,1,1.71,0l6.723,11.073A1,1,0,0,1,15.223,14H1.777a1,1,0,0,1-.855-1.519Z" | ||
transform="translate(17 14) rotate(180)" | ||
fill="#007aff" | ||
/> | ||
</svg> | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이걸 보니 Icon 도 cos-ui 에 추가해야될까? 라는 생각이 들긴 하네요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 개수가 적더라도 추가하는거도 괜찮다고 생각해요! |
||
|
||
const ColumnTablePage = () => ( | ||
<ColumnTable | ||
caption="공지사항" | ||
columnsWidth={['85px', '*', '120px', '120px', '120px']} | ||
sortValues={sortValues} | ||
> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rem 은 적용이 안되는거겠죠?? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 html에 지정하는 width는 px단위 밖에 안되서요ㅠㅠ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 도니 생각에 사용자가 rem, 혹은 다른 단위로 넣고싶을 거라고 느낀다면 ColumnTable 내부에서 여러 단위에 대해 대응해도 좋다고 생각해요 |
||
<ColumnTable.Row> | ||
<ColumnTable.HeadCell> | ||
<Text as="div" sx={{ textAlign: 'center' }}> | ||
번호 | ||
</Text> | ||
</ColumnTable.HeadCell> | ||
<ColumnTable.HeadCell>제목</ColumnTable.HeadCell> | ||
<ColumnTable.HeadCell name="writer"> | ||
<FlexBox | ||
sx={{ justifyContent: 'center', alignItems: 'center', gap: '1rem' }} | ||
> | ||
작성자 | ||
<SortIcon /> | ||
</FlexBox> | ||
</ColumnTable.HeadCell> | ||
<ColumnTable.HeadCell name="date"> | ||
<FlexBox | ||
sx={{ justifyContent: 'center', alignItems: 'center', gap: '1rem' }} | ||
> | ||
날짜 | ||
<SortIcon /> | ||
</FlexBox> | ||
</ColumnTable.HeadCell> | ||
<ColumnTable.HeadCell name="view"> | ||
<FlexBox | ||
sx={{ justifyContent: 'center', alignItems: 'center', gap: '1rem' }} | ||
> | ||
조회수 | ||
<SortIcon /> | ||
</FlexBox> | ||
</ColumnTable.HeadCell> | ||
</ColumnTable.Row> | ||
{noticeList.map(({ id, title, writer, date, view }) => ( | ||
<ColumnTable.Row key={id}> | ||
<ColumnTable.BodyCell> | ||
<Text sx={{ textAlign: 'center' }} ellipsis> | ||
{id} | ||
</Text> | ||
</ColumnTable.BodyCell> | ||
<ColumnTable.BodyCell> | ||
<Text ellipsis>{title}</Text> | ||
</ColumnTable.BodyCell> | ||
<ColumnTable.BodyCell> | ||
<Text sx={{ textAlign: 'center' }} ellipsis> | ||
{writer} | ||
</Text> | ||
</ColumnTable.BodyCell> | ||
<ColumnTable.BodyCell> | ||
<Text sx={{ textAlign: 'center' }} ellipsis> | ||
{date} | ||
</Text> | ||
</ColumnTable.BodyCell> | ||
<ColumnTable.BodyCell> | ||
<Text sx={{ textAlign: 'center' }} ellipsis> | ||
{view} | ||
</Text> | ||
</ColumnTable.BodyCell> | ||
</ColumnTable.Row> | ||
))} | ||
</ColumnTable> | ||
); | ||
|
||
export default ColumnTablePage; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { ReactElement, ReactNode } from 'react'; | ||
|
||
import { offscreen } from '@styles/commonStyles'; | ||
|
||
export type BasicTableProps = { | ||
caption: string; | ||
columnsWidth?: string[]; | ||
children: (ReactElement | ReactElement[])[]; | ||
}; | ||
|
||
const BasicTable = ({ | ||
caption, | ||
columnsWidth, | ||
children, | ||
...restProps | ||
}: BasicTableProps) => ( | ||
<table {...restProps}> | ||
<caption css={offscreen}>{caption}</caption> | ||
{columnsWidth && ( | ||
<colgroup> | ||
{columnsWidth.map((width, index) => ( | ||
// 유동적으로 변하지 않는 리스트 | ||
// eslint-disable-next-line react/no-array-index-key | ||
<col key={index} width={width} /> | ||
))} | ||
</colgroup> | ||
)} | ||
<tbody>{children}</tbody> | ||
</table> | ||
); | ||
|
||
type RowProps = { | ||
children: ReactElement | ReactElement[]; | ||
}; | ||
|
||
const Row = ({ children, ...restProps }: RowProps) => ( | ||
<tr {...restProps}>{children}</tr> | ||
); | ||
|
||
type CellProps = { | ||
colSpan?: number; | ||
rowSpan?: number; | ||
children: ReactNode; | ||
}; | ||
|
||
const Cell = ({ children, ...restProps }: CellProps) => ( | ||
<td {...restProps}>{children}</td> | ||
); | ||
|
||
BasicTable.Row = Row; | ||
BasicTable.Cell = Cell; | ||
|
||
export default BasicTable; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { ReactNode, Dispatch, SetStateAction } from 'react'; | ||
|
||
const DIRECTION = { | ||
ASCENDING: 'ascending', | ||
DESCENDING: 'descending', | ||
}; | ||
|
||
export type SortState = { | ||
name: string; | ||
direction: typeof DIRECTION[keyof typeof DIRECTION]; | ||
sortIndices: number[]; | ||
} | null; | ||
|
||
export type SortConfig = { | ||
sortValues?: { [key in string]: (string | number)[] }; | ||
sortState: SortState; | ||
setSortState: Dispatch<SetStateAction<SortState>>; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 별 건 아니지만 문득 도니가 사용하신 [key in string]과 [key: string]이 무슨 차이인지 궁금해서 찾아봤었는데요! 제 생각엔 [key: string] 쪽이 좀 더 적절하지 않나 싶었던 것 같아요. 그리고 sortState가 현재 어떤 것을 기준으로 정렬하고 있는지에 관한 정보니까 이런 부분을 좀 더 명확히 나타낼 수 있는 이름이면 더 좋겠다는 생각도 드네요! |
||
|
||
type CellProps = { | ||
colSpan?: number; | ||
rowSpan?: number; | ||
name?: string; | ||
sortConfig?: SortConfig; | ||
children: ReactNode; | ||
}; | ||
|
||
interface HeadCellProps extends CellProps { | ||
scope?: 'col' | 'row'; | ||
sortConfig: SortConfig; | ||
} | ||
|
||
const HeadCell = ({ | ||
name = '', | ||
scope, | ||
sortConfig, | ||
children, | ||
...restProps | ||
}: HeadCellProps) => { | ||
const { sortValues = {}, sortState, setSortState } = sortConfig; | ||
const direction = | ||
sortState && | ||
sortState.name === name && | ||
sortState.direction === DIRECTION.DESCENDING | ||
? DIRECTION.ASCENDING | ||
: DIRECTION.DESCENDING; | ||
|
||
const getSortIndices = () => { | ||
const sortValuesByName = sortValues[name].map((value, index) => ({ | ||
value, | ||
index, | ||
})); | ||
sortValuesByName.sort((a, b) => { | ||
if (a.value > b.value) return direction === DIRECTION.ASCENDING ? 1 : -1; | ||
if (a.value < b.value) return direction === DIRECTION.ASCENDING ? -1 : 1; | ||
return 0; | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이것 보고 검색해보니 localeCompare 라는 것도 있네요 sortValuesByName 은 배열인데 sort~ 라는건 특정 동작을 하는 함수이지 않을까? 라는 생각도 들었습니다. sorted 정도로 넣으면 어떤가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 안그래도 이 부분 이름 때문에 고민을 정말 많이 했는데요..😂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 메서드 체이닝으로 바로 사용해서 sorted 로 넣어도 좋을 것 같아요~! |
||
|
||
return sortValuesByName.map(({ index }) => index); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updateSortState를 실행할 때 매번 이 연산을 수행하게 되는데 같은 컬럼을 여러번 클릭하는 경우에는 매번 계산하는 게 불필요할 수 있을 것 같아요! 현재 sortState에 있는 name과 같다면 가지고 있는 sortIndices를 reverse해서 반환해줘도 되지 않을까요? |
||
|
||
const updateSortState = () => { | ||
if (!sortValues[name]) | ||
throw new Error('Please set sortValues[name] in Table props.'); | ||
|
||
const newSortState = { | ||
name, | ||
direction, | ||
sortIndices: getSortIndices(), | ||
}; | ||
setSortState(newSortState); | ||
}; | ||
|
||
return ( | ||
<th | ||
scope={scope} | ||
data-sort={sortState?.name === name ? sortState?.direction : undefined} | ||
{...restProps} | ||
> | ||
{name ? ( | ||
<button | ||
type="button" | ||
css={{ display: 'block', width: '100%' }} | ||
onClick={updateSortState} | ||
> | ||
{children} | ||
</button> | ||
) : ( | ||
children | ||
)} | ||
</th> | ||
); | ||
}; | ||
|
||
const BodyCell = ({ children, ...restProps }: CellProps) => ( | ||
<td {...restProps}>{children}</td> | ||
); | ||
|
||
const Cell = ({ name, sortConfig, children, ...restProps }: CellProps) => | ||
sortConfig ? ( | ||
<HeadCell sortConfig={sortConfig} name={name} {...restProps}> | ||
{children} | ||
</HeadCell> | ||
) : ( | ||
<BodyCell {...restProps}>{children}</BodyCell> | ||
); | ||
|
||
export default Cell; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import React, { cloneElement, ReactElement, useState } from 'react'; | ||
|
||
import Cell, { SortState, SortConfig } from '@components/Table/Cell'; | ||
import { offscreen } from '@styles/commonStyles'; | ||
|
||
export type ColumnTableProps = { | ||
caption: string; | ||
columnsWidth?: string[]; | ||
sortValues?: { [key in string]: (string | number)[] }; | ||
children: (ReactElement | ReactElement[])[]; | ||
}; | ||
|
||
const ColumnTable = ({ | ||
caption, | ||
columnsWidth, | ||
sortValues, | ||
children, | ||
...restProps | ||
}: ColumnTableProps) => { | ||
const Children = React.Children.toArray(children); | ||
const [headRow, bodyRows] = [Children[0], Children.splice(1)]; | ||
const [sortState, setSortState] = useState<SortState>(null); | ||
const sortConfig = { sortValues, sortState, setSortState }; | ||
|
||
return ( | ||
<table {...restProps}> | ||
<caption css={offscreen}>{caption}</caption> | ||
{columnsWidth && ( | ||
<colgroup> | ||
{columnsWidth.map((width, index) => ( | ||
// 유동적으로 변하지 않는 리스트 | ||
// eslint-disable-next-line react/no-array-index-key | ||
<col key={index} width={width} /> | ||
))} | ||
</colgroup> | ||
)} | ||
<thead> | ||
{cloneElement(headRow as ReactElement, { scope: 'col', sortConfig })} | ||
</thead> | ||
<tbody> | ||
{sortState | ||
? sortState.sortIndices.map((index) => bodyRows[index]) | ||
: bodyRows} | ||
</tbody> | ||
</table> | ||
); | ||
}; | ||
|
||
type RowProps = { | ||
scope?: 'col'; | ||
sortConfig?: SortConfig; | ||
children: ReactElement | ReactElement[]; | ||
}; | ||
|
||
const Row = ({ scope, sortConfig, children, ...restProps }: RowProps) => ( | ||
<tr {...restProps}> | ||
{sortConfig | ||
? React.Children.toArray(children).map((child) => | ||
cloneElement(child as ReactElement, { scope, sortConfig }), | ||
) | ||
: children} | ||
</tr> | ||
); | ||
|
||
ColumnTable.Row = Row; | ||
ColumnTable.Cell = Cell; | ||
|
||
export default ColumnTable; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
constants 등으로 따로 분리해도 좋을 것 같네요~