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

[Dony] Table 컴포넌트 구현 #7

Merged
merged 18 commits into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
128 changes: 128 additions & 0 deletions packages/playground-react/pages/table/column-table.tsx
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,
},
];
Copy link
Member

Choose a reason for hiding this comment

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

constants 등으로 따로 분리해도 좋을 것 같네요~


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;
}),
Copy link
Member

Choose a reason for hiding this comment

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

지금 date 형태가 "YYYY.MM.DD" 인데, 표준으로 쓰이는 Date 의 형태를 쓰는게 어떤가요? (저희 서버에서는 new Date().getTime() 한 값과 동일한 형태로 줍니다)

사용자가 원하는 형태로 쓸 수 있다는 유연함을 줄 수있는 방식이라고 생각해서 방식 자체는 좋다고 생각합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>
);
Copy link
Member

Choose a reason for hiding this comment

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

이걸 보니 Icon 도 cos-ui 에 추가해야될까? 라는 생각이 들긴 하네요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

저는 개수가 적더라도 추가하는거도 괜찮다고 생각해요!


const ColumnTablePage = () => (
<ColumnTable
caption="공지사항"
columnsWidth={['85px', '*', '120px', '120px', '120px']}
sortValues={sortValues}
>
Copy link
Member

Choose a reason for hiding this comment

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

rem 은 적용이 안되는거겠죠??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵 html에 지정하는 width는 px단위 밖에 안되서요ㅠㅠ
흠.. rem으로 지정하면 px로 계산하는 로직을 추가할까요..??

Copy link
Member

Choose a reason for hiding this comment

The 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;
53 changes: 53 additions & 0 deletions packages/primitives/src/components/Table/BasicTable.tsx
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;
108 changes: 108 additions & 0 deletions packages/primitives/src/components/Table/Cell/index.tsx
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>>;
};
Copy link
Member

Choose a reason for hiding this comment

The 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;
});
Copy link
Member

Choose a reason for hiding this comment

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

이것 보고 "가" > "나" 등의 연산이 된다는걸 알았네요 ㅋㅋ
코드를 읽고 조금 생각해봤을 때 문자열 비교가 필요하기도 하고, 숫자 비교도 필요해서 둘 다 할 수있는 방식을 사용했다고 생각했어요
별건 아니지만, 어떤 의도였는지도 코드에서 선언적으로 바로 보이면 좋겠다는 생각도 했어요.

검색해보니 localeCompare 라는 것도 있네요

sortValuesByName 은 배열인데 sort~ 라는건 특정 동작을 하는 함수이지 않을까? 라는 생각도 들었습니다. sorted 정도로 넣으면 어떤가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

안그래도 이 부분 이름 때문에 고민을 정말 많이 했는데요..😂
sorted는 정렬된 이라는 형용사로 쓰여 정렬 후에 붙여주는게 맞더라구여.
sort는 정렬, 정렬하다 동명사 둘 다 해당이 되어 문법적으로는 맞는데 동사로 헷갈릴 여지가 있어 고민했습니다ㅠ
참고한 라이브러리에서는 sortConfig라고 사용하고 있길래 그냥 사용하였는데 sortingValues 로 바꿔볼게요!

Copy link
Member

Choose a reason for hiding this comment

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

메서드 체이닝으로 바로 사용해서 sorted 로 넣어도 좋을 것 같아요~!


return sortValuesByName.map(({ index }) => index);
};
Copy link
Member

Choose a reason for hiding this comment

The 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;
68 changes: 68 additions & 0 deletions packages/primitives/src/components/Table/ColumnTable.tsx
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;