Skip to content

Commit

Permalink
[Feat] 인수인계 노트 페이지 퍼블리싱 (#323)
Browse files Browse the repository at this point in the history
* feat: NoteItem 컴포넌트 구현

* fix: select의 option variant 스타일 수정

* feat: HandoverPage 구현

* feat: 정렬, 전체 선택 기능 구현

* feat: css grid 적용

* refactor: 네이밍 리펙토링

* style: 노트리스트 헤더 글자색 수정

* fix: 코드리뷰 반영

* fix: conflict 해결

* fix: useMultiSelected 반영

* fix: story 수정

* fix: 논메 리드님 의견 반영

* fix: 주용이 의견 반영

* fix: 핸들러 네이밍 변경
  • Loading branch information
rtttr1 authored Nov 20, 2024
1 parent 5b075d9 commit bb391b3
Show file tree
Hide file tree
Showing 14 changed files with 388 additions and 27 deletions.
5 changes: 5 additions & 0 deletions src/common/asset/svg/ic_more.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/common/asset/svg/ic_sticky_note_2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions src/common/component/Select/Select.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,13 @@ export const triggerStyle = (variant: Required<SelectProps['variant']>, isSelect
backgroundColor: theme.colors.white,

whiteSpace: 'nowrap',
cursor: 'pointer',

'& > span': {
...theme.text.body06,

width: '80%',
textAlign: 'start',
textOverflow: 'ellipsis',
overflow: 'hidden',
},
},
/** underline 있는 select trigger 버튼 */
Expand All @@ -101,8 +100,11 @@ export const triggerStyle = (variant: Required<SelectProps['variant']>, isSelect
/** "최근 업로드 순"과 같은 option select */
variant === 'option'
? {
height: '3.2rem',
padding: '1rem 0.1rem 1rem 1rem',

justifyContent: 'flex-end',
gap: '0.2rem',
gap: '0.4rem',

fontWeight: 400,
color: theme.colors.gray_800,
Expand Down
22 changes: 18 additions & 4 deletions src/common/hook/useMultiSelect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';

type DataHasIdKey<T> = {
[key in keyof T]: T[key];
Expand All @@ -10,6 +10,7 @@ type DataHasIdKey<T> = {
*/
export const useMultiSelect = <T extends object>(identifier: keyof T, data: DataHasIdKey<T>[]) => {
const [ids, setIds] = useState<number[]>([]);
const [canSelect, setCanSelect] = useState(false);

const handleItemClick = (id: number) => {
if (ids.includes(id)) {
Expand All @@ -35,9 +36,22 @@ export const useMultiSelect = <T extends object>(identifier: keyof T, data: Data
}
};

const handleReset = () => {
setIds([]);
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Escape' && canSelect) {
setCanSelect(false);

setIds([]);
}
};
window.addEventListener('keydown', handleKeyPress);

return () => window.removeEventListener('keydown', handleKeyPress);
}, [canSelect]);

const handleToggleSelect = () => {
setCanSelect((prev) => !prev);
};

return { ids, handleItemClick, handleAllClick, handleReset };
return { ids, canSelect, handleItemClick, handleAllClick, handleToggleSelect };
};
4 changes: 3 additions & 1 deletion src/common/router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
UnivFormPage,
} from '@/common/router/lazy';

import HandoverPage from '@/page/handover/HandoverPage';

import { PATH } from '@/shared/constant/path';

const Public = () => {
Expand Down Expand Up @@ -157,7 +159,7 @@ const router = createBrowserRouter([
path: PATH.HANDOVER,
element: (
<Suspense>
<h1>HandOver</h1>
<HandoverPage />
</Suspense>
),
},
Expand Down
24 changes: 5 additions & 19 deletions src/page/deleted/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { useEffect, useState } from 'react';

import Button from '@/common/component/Button/Button';
import Flex from '@/common/component/Flex/Flex';
import Select from '@/common/component/Select/Select';
Expand Down Expand Up @@ -27,22 +25,10 @@ const tmpData: File[] = [

const DeletedPage = () => {
const { isOpen, toggle } = useOverlay();
const { ids, handleItemClick, handleAllClick, handleReset } = useMultiSelect<File>('fileId', tmpData);

const [canSelect, setCanSelect] = useState(false);

useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Escape' && canSelect) {
setCanSelect(false);

handleReset();
}
};
window.addEventListener('keydown', handleKeyPress);

return () => window.removeEventListener('keydown', handleKeyPress);
}, [canSelect, handleReset]);
const { ids, canSelect, handleItemClick, handleAllClick, handleToggleSelect } = useMultiSelect<File>(
'fileId',
tmpData
);

return (
<ContentBox
Expand All @@ -61,7 +47,7 @@ const DeletedPage = () => {
<Button variant="tertiary">영구삭제</Button>
</Flex>
) : (
<Button onClick={() => setCanSelect(true)} variant="tertiary">
<Button onClick={handleToggleSelect} variant="tertiary">
선택
</Button>
)}
Expand Down
31 changes: 31 additions & 0 deletions src/page/handover/HandoverPage.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { css } from '@emotion/react';

import { theme } from '@/common/style/theme/theme';

export const periodStyle = (activeSelect: boolean) =>
css({
width: '26rem',
marginRight: activeSelect ? '0.8rem' : '4.2rem',

color: theme.colors.gray_500,
});

export const titleStyle = css({
width: '34rem',
marginRight: '27.6rem',

color: theme.colors.gray_500,
});

export const writerStyle = css({
width: '10.4rem',
marginRight: '3.4rem',

color: theme.colors.gray_500,
});

export const finishedStyle = css({
width: '6.1rem',

color: theme.colors.gray_500,
});
99 changes: 99 additions & 0 deletions src/page/handover/HandoverPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useState } from 'react';

import IcSearch from '@/common/asset/svg/ic_search.svg?react';
import Button from '@/common/component/Button/Button';
import Divider from '@/common/component/Divider/Divider';
import Flex from '@/common/component/Flex/Flex';
import Input from '@/common/component/Input/Input';
import Select from '@/common/component/Select/Select';
import { useOutsideClick, useOverlay } from '@/common/hook';
import { useMultiSelect } from '@/common/hook/useMultiSelect';

import NoteItem from '@/page/handover/component/NoteItem/NoteItem';
import NoteListHeader from '@/page/handover/component/NoteListHeader/NoteListHeader';
import { FILTER_OPTION, NOTE_DUMMY } from '@/page/handover/constant/noteList';

import ContentBox from '@/shared/component/ContentBox/ContentBox';

const HandoverPage = () => {
const [sortOption, setSortOption] = useState('');
const [searchValue, setSearchValue] = useState('');

const { isOpen, close, toggle } = useOverlay();
const ref = useOutsideClick<HTMLDivElement>(close);

const { ids, canSelect, handleItemClick, handleAllClick, handleToggleSelect } = useMultiSelect<
(typeof NOTE_DUMMY)[0]
>('id', NOTE_DUMMY);

const handleSortOption = (id: string) => {
setSortOption(id);

close();
};

return (
<ContentBox
variant="handover"
title="인수인계 노트"
headerOption={
<Flex styles={{ align: 'center', gap: '0.8rem' }}>
<Input
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
css={{ width: '33.6rem' }}
LeftIcon={<IcSearch width={16} height={16} />}
placeholder="노트 제목을 검색해보세요"
/>
<Button>새 노트 작성</Button>
</Flex>
}
contentOption={
<Flex styles={{ width: '100%', justify: 'space-between', align: 'center', gap: '1rem' }}>
<Flex styles={{ gap: '0.8rem' }}>
<Button variant="tertiary" onClick={handleToggleSelect}>
선택
</Button>
</Flex>

<Flex styles={{ align: 'center' }}>
<Select
aria-label={`선택된 아이템: ${sortOption}`}
css={{ width: '10.5rem' }}
placeholder="최근 작성된 순"
variant="option"
options={FILTER_OPTION}
ref={ref}
isOpen={isOpen}
onTrigger={toggle}
onSelect={handleSortOption}
/>
</Flex>
</Flex>
}>
<NoteListHeader
isSelected={ids.length === NOTE_DUMMY.length}
canSelect={canSelect}
handleAllClick={handleAllClick}
/>
<Divider />
<ul>
{(sortOption === FILTER_OPTION[0].value ? NOTE_DUMMY.slice() : NOTE_DUMMY.slice().reverse()).map((data) => (
<NoteItem
key={data.id}
startDate={data.startDate}
endDate={data.endDate}
title={data.title}
writer={data.writer}
isFinished={data.isFinished}
canSelect={canSelect}
isSelected={ids.includes(+data.id)}
onSelect={() => handleItemClick(+data.id)}
/>
))}
</ul>
</ContentBox>
);
};

export default HandoverPage;
30 changes: 30 additions & 0 deletions src/page/handover/component/NoteItem/NoteItem.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { css } from '@emotion/react';

import { theme } from '@/common/style/theme/theme';

export const containerStyle = css({
display: 'grid',
gridTemplateColumns: '25.3% 51.7% 11.6% 11.4%',

width: '100%',
margin: '1.6rem 0 ',
minHeight: '1.8rem',

'& *': {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
});

export const profileStyle = css({
width: '2.2rem',
heigth: '2.2rem',

borderRadius: '100%',
backgroundColor: theme.colors.gray_300,
});

export const moreButtonStyle = css({
cursor: 'pointer',
});
75 changes: 75 additions & 0 deletions src/page/handover/component/NoteItem/NoteItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import avartar from '@/common/asset/svg/ic_avatar.svg';
import MoreIcButton from '@/common/asset/svg/ic_more.svg?react';
import CheckBox from '@/common/component/CheckBox/CheckBox';
import Divider from '@/common/component/Divider/Divider';
import Flex from '@/common/component/Flex/Flex';
import Tag from '@/common/component/Tag/Tag';
import Text from '@/common/component/Text/Text';
import { theme } from '@/common/style/theme/theme';

import { formattingDate } from '@/page/archiving/index/util/date';
import { containerStyle, moreButtonStyle, profileStyle } from '@/page/handover/component/NoteItem/NoteItem.style';

interface NoteItemProps {
startDate: Date;
endDate: Date;
title: string;
writer: string;
isFinished: boolean;
canSelect: boolean;
isSelected: boolean;
onSelect: () => void;
}

const NoteItem = ({
startDate,
endDate,
title,
writer,
isFinished,
canSelect,
isSelected,
onSelect,
}: NoteItemProps) => {
return (
<li>
<Flex styles={{ align: 'center' }}>
<Flex styles={{ align: 'center', justify: 'left' }} css={containerStyle}>
<Flex styles={{ align: 'center' }}>
{canSelect && (
<CheckBox isChecked={isSelected} onChange={() => onSelect?.()} style={{ marginRight: '1.6rem' }} />
)}
<Text tag="body6" style={{ width: '26rem' }}>
{`${formattingDate(startDate)} - ${formattingDate(endDate)}`}
</Text>
</Flex>

<Text tag="body6" style={{ width: '34rem' }}>
{title}
</Text>
<Flex styles={{ align: 'center', gap: '0.4rem' }}>
<img src={avartar} alt="작성자 프로필" css={profileStyle} />
<Text tag="body6" style={{ width: '10.4rem' }}>
{writer}
</Text>
</Flex>
<Flex styles={{ align: 'center', gap: isFinished ? '4.3rem' : '3.3rem' }}>
{isFinished ? (
<Tag variant="square" bgColor={theme.colors.key_400}>
작성 완료
</Tag>
) : (
<Tag variant="square" bgColor={theme.colors.gray_300}>
작성 미완료
</Tag>
)}
<MoreIcButton width={18} height={18} css={moreButtonStyle} />
</Flex>
</Flex>
</Flex>
<Divider color={theme.colors.gray_300} />
</li>
);
};

export default NoteItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { css } from '@emotion/react';

import { theme } from '@/common/style/theme/theme';

export const fontStyle = css({
'& p': {
color: theme.colors.gray_500,
},
});
Loading

0 comments on commit bb391b3

Please sign in to comment.