Skip to content

Commit 13d1292

Browse files
authored
Merge pull request #206 from mash-up-kr/release/v1.1.0
Main Release/v1.1.0
2 parents bb266c1 + 1164488 commit 13d1292

File tree

10 files changed

+208
-131
lines changed

10 files changed

+208
-131
lines changed

src/components/common/SearchOptionBar/SearchOptionBar.component.tsx

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React, { Dispatch, SetStateAction, useLayoutEffect, useMemo, useRef } from 'react';
1+
import React, { FormEvent, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2+
import { useSearchParams } from 'react-router-dom';
23
import { Input, Select } from '@/components';
34
import { ButtonSize, ButtonShape } from '@/components/common/Button/Button.component';
45
import * as Styled from './SearchOptionBar.styled';
@@ -17,42 +18,71 @@ export interface ApplicationFilterValuesType {
1718

1819
interface SearchOptionBarProps {
1920
placeholder?: string;
20-
searchWord: { value: string };
21-
handleSubmit: React.FormEventHandler<HTMLFormElement>;
22-
filterValues?: ApplicationFilterValuesType;
23-
setFilterValues?: Dispatch<SetStateAction<ApplicationFilterValuesType>>;
21+
showFilterValues?: boolean;
2422
}
2523

2624
const DEFAULT = { label: '전체', value: '' };
2725

28-
const SearchOptionBar = ({
29-
placeholder,
30-
searchWord,
31-
handleSubmit,
32-
filterValues,
33-
setFilterValues,
34-
}: SearchOptionBarProps) => {
26+
const SearchOptionBar = ({ placeholder, showFilterValues = true }: SearchOptionBarProps) => {
27+
const [searchParams, setSearchParams] = useSearchParams();
3528
const ref = useRef<HTMLInputElement>(null);
3629

30+
const confirmStatus = searchParams.get('confirmStatus') || '';
31+
const resultStatus = searchParams.get('resultStatus') || '';
32+
const searchWord = searchParams.get('searchWord') || '';
33+
34+
const [filterValues, setFilterValues] = useState<ApplicationFilterValuesType>({
35+
confirmStatus: { label: '', value: '' },
36+
resultStatus: { label: '', value: '' },
37+
});
38+
39+
const [searchKeyword, setSearchKeyword] = useState<{ value: string }>({ value: '' });
40+
3741
const handleApplicationConfirmStatus = (option: SelectOption) => {
38-
setFilterValues?.((prev) => ({
39-
...prev,
40-
confirmStatus: option,
41-
}));
42+
if (option.value === DEFAULT.value) {
43+
searchParams.delete('confirmStatus');
44+
setSearchParams(searchParams);
45+
return;
46+
}
47+
48+
searchParams.set('confirmStatus', option.value);
49+
setSearchParams(searchParams);
4250
};
4351

4452
const handleApplicationResultStatus = (option: SelectOption) => {
45-
setFilterValues?.((prev) => ({
46-
...prev,
47-
resultStatus: option,
48-
}));
53+
if (option.value === DEFAULT.value) {
54+
searchParams.delete('resultStatus');
55+
setSearchParams(searchParams);
56+
return;
57+
}
58+
59+
searchParams.set('resultStatus', option.value);
60+
setSearchParams(searchParams);
61+
};
62+
63+
const handleSearch = (
64+
e: { target: { searchWord: { value: string } } } & FormEvent<HTMLFormElement>,
65+
) => {
66+
const { value } = e.target.searchWord;
67+
68+
e.preventDefault();
69+
70+
if (!value) {
71+
searchParams.delete('searchWord');
72+
setSearchParams(searchParams);
73+
74+
return;
75+
}
76+
77+
searchParams.set('searchWord', value);
78+
setSearchParams(searchParams);
4979
};
5080

5181
useLayoutEffect(() => {
5282
if (ref.current) {
53-
ref.current.value = searchWord.value;
83+
ref.current.value = searchKeyword.value;
5484
}
55-
}, [searchWord]);
85+
}, [searchKeyword]);
5686

5787
const applicationConfirmStatusOptions = useMemo(
5888
() => [
@@ -88,9 +118,31 @@ const SearchOptionBar = ({
88118
[],
89119
);
90120

121+
useEffect(() => {
122+
const confirmStatusLabel =
123+
applicationConfirmStatusOptions.find((option) => option.value === confirmStatus)?.label ?? '';
124+
125+
const resultStatusLabel =
126+
applicationResultStatusOptions.find((option) => option.value === resultStatus)?.label ?? '';
127+
128+
setFilterValues({
129+
confirmStatus: { label: confirmStatusLabel, value: confirmStatus },
130+
resultStatus: { label: resultStatusLabel, value: resultStatus },
131+
});
132+
}, [
133+
applicationConfirmStatusOptions,
134+
applicationResultStatusOptions,
135+
confirmStatus,
136+
resultStatus,
137+
]);
138+
139+
useEffect(() => {
140+
setSearchKeyword({ value: searchWord });
141+
}, [searchWord]);
142+
91143
return (
92-
<Styled.BarContainer onSubmit={handleSubmit}>
93-
{filterValues && (
144+
<Styled.BarContainer onSubmit={handleSearch}>
145+
{showFilterValues && (
94146
<Styled.SelectContainer>
95147
<div>
96148
<div>합격여부</div>

src/hooks/useConvertToXlsx.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ interface UseExportXlsxProps<T> {
88
}
99

1010
const useConvertToXlsx = <T>({ workSheet, teamName, isLoading }: UseExportXlsxProps<T>) => {
11-
const workBookRef = useRef<WorkBook>(utils.book_new());
11+
const workBookRef = useRef<WorkBook>();
12+
1213
useEffect(() => {
1314
if (workSheet) {
1415
workBookRef.current = utils.book_new();
@@ -17,7 +18,7 @@ const useConvertToXlsx = <T>({ workSheet, teamName, isLoading }: UseExportXlsxPr
1718
}
1819
}, [teamName, workSheet, isLoading]);
1920

20-
return { workBook: workBookRef.current };
21+
return { getWorkBook: () => workBookRef.current };
2122
};
2223

2324
export default useConvertToXlsx;

src/hooks/useHistory.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { act } from '@testing-library/react';
2+
import { renderHook } from '@testing-library/react-hooks';
3+
4+
import useHistory from './useHistory';
5+
6+
const mockNavigate = jest.fn();
7+
8+
const mockUseLocation = jest.fn().mockImplementation(() => ({ state: {} }));
9+
10+
jest.mock('react-router-dom', () => ({
11+
useLocation: () => mockUseLocation(),
12+
useNavigate: jest.fn().mockImplementation(() => mockNavigate),
13+
}));
14+
15+
describe('useHistory', () => {
16+
it('from 값이 존재한다면 뒤로가기 클릭 시 from으로 이동한다', () => {
17+
// Given
18+
const from = 'from';
19+
mockUseLocation.mockReturnValueOnce({
20+
state: {
21+
from,
22+
},
23+
});
24+
25+
// When
26+
const { result } = renderHook(() => useHistory());
27+
act(() => result.current.handleGoBack());
28+
29+
// Then
30+
expect(mockNavigate).toBeCalledWith(from);
31+
});
32+
33+
it('from 값이 존재하지 않고 defaultPath가 있다면 defaultPath로 이동한다', () => {
34+
// Given
35+
const defaultPath = 'default-path';
36+
37+
// When
38+
const { result } = renderHook(() => useHistory());
39+
act(() => result.current.handleGoBack(defaultPath));
40+
41+
// Then
42+
expect(mockNavigate).toBeCalledWith(defaultPath);
43+
});
44+
45+
it('from 값과 defaultPath값 모두 존재하지 않는다면 뒤로 이동한다', () => {
46+
// Given
47+
48+
// When
49+
const { result } = renderHook(() => useHistory());
50+
act(() => result.current.handleGoBack());
51+
52+
// Then
53+
expect(mockNavigate).toBeCalledWith(-1);
54+
});
55+
56+
it('shouldClearQueryString값이 false라면 from값에 쿼리스트링도 함께 저장된다.', () => {
57+
// Given
58+
const from = '/from';
59+
const queryString = '?queryString=queryString';
60+
const fromWithQueryString = `${from}${queryString}`;
61+
const to = '/to';
62+
63+
mockUseLocation.mockReturnValueOnce({
64+
pathname: from,
65+
search: queryString,
66+
});
67+
68+
// When
69+
const { result } = renderHook(() => useHistory(false));
70+
act(() => result.current.handleNavigate(to));
71+
72+
// Then
73+
74+
expect(mockNavigate).toBeCalledWith(to, { state: { from: fromWithQueryString } });
75+
});
76+
});

src/hooks/useHistory.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ import { useLocation, useNavigate } from 'react-router-dom';
22

33
type HistoryLocationFromState = { from: string } | undefined;
44

5-
const useHistory = () => {
5+
const useHistory = (shouldClearQueryString = true) => {
66
const navigate = useNavigate();
77
const location = useLocation();
8+
const { search } = location ?? {};
89
const { from } = (location.state as HistoryLocationFromState) ?? {};
910

1011
const handleNavigate = (to: string) => {
12+
const currentPath = location.pathname;
13+
const currentPathWithQueryString = `${currentPath}${search}`;
14+
const fromPath = shouldClearQueryString ? currentPath : currentPathWithQueryString;
15+
1116
navigate(to, {
1217
state: {
13-
from: location.pathname,
18+
from: fromPath,
1419
},
1520
});
1621
};

src/pages/ApplicationFormList/ApplicationFormList.page.tsx

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { FormEvent, useEffect, useLayoutEffect, useMemo, useState } from 'react';
1+
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react';
22
import { useRecoilStateLoadable, useRecoilValue } from 'recoil';
33
import { useLocation, useSearchParams } from 'react-router-dom';
44

@@ -42,14 +42,13 @@ const ApplicationFormPreview = ({ questions }: { questions: Question[] }) => {
4242
};
4343

4444
const ApplicationFormList = () => {
45-
const [searchParams] = useSearchParams();
45+
const [searchParams, setSearchParams] = useSearchParams();
4646
const teamName = searchParams.get('team');
4747
const teamId = useRecoilValue($teamIdByName(teamName));
4848

4949
const page = searchParams.get('page') || '1';
5050
const size = searchParams.get('size') || '20';
51-
52-
const [searchWord, setSearchWord] = useState<{ value: string }>({ value: '' });
51+
const searchWord = searchParams.get('searchWord') || '';
5352

5453
const { pathname, search } = useLocation();
5554

@@ -72,7 +71,7 @@ const ApplicationFormList = () => {
7271
page: parseInt(page, 10) - 1,
7372
size: parseInt(size, 10),
7473
teamId: parseInt(teamId, 10) || undefined,
75-
searchWord: searchWord.value,
74+
searchWord,
7675
sort: sortParam,
7776
}),
7877
[page, size, teamId, searchWord, sortParam],
@@ -94,13 +93,6 @@ const ApplicationFormList = () => {
9493

9594
const { makeDirty, isDirty } = useDirty(1);
9695

97-
const handleSubmit = (
98-
e: { target: { searchWord: { value: string } } } & FormEvent<HTMLFormElement>,
99-
) => {
100-
e.preventDefault();
101-
setSearchWord({ value: e.target.searchWord.value });
102-
};
103-
10496
const columns: TableColumn<ApplicationFormResponse>[] = [
10597
{
10698
title: '플랫폼',
@@ -167,13 +159,14 @@ const ApplicationFormList = () => {
167159
}, [isLoading, tableRows]);
168160

169161
useEffect(() => {
170-
setSearchWord({ value: '' });
162+
searchParams.delete('searchWord');
163+
setSearchParams(searchParams);
171164
// eslint-disable-next-line react-hooks/exhaustive-deps
172165
}, [teamName]);
173166

174167
useLayoutEffect(() => {
175168
if (isDirty && !isLoading) {
176-
window.scrollTo(0, 179);
169+
window.scrollTo({ top: 179, left: 0, behavior: 'smooth' });
177170
}
178171
// eslint-disable-next-line react-hooks/exhaustive-deps
179172
}, [loadedTableRows]);
@@ -183,11 +176,7 @@ const ApplicationFormList = () => {
183176
<Styled.Heading>지원서 설문지 내역</Styled.Heading>
184177
<Styled.StickyContainer>
185178
<TeamNavigationTabs />
186-
<SearchOptionBar
187-
placeholder="지원서 설문지 문서명 검색"
188-
searchWord={searchWord}
189-
handleSubmit={handleSubmit}
190-
/>
179+
<SearchOptionBar placeholder="지원서 설문지 문서명 검색" showFilterValues={false} />
191180
</Styled.StickyContainer>
192181
<Table
193182
prefix="application-form"

0 commit comments

Comments
 (0)