Skip to content

Commit

Permalink
Merge pull request #319 from GDSC-PKNU-Official/feat/#315
Browse files Browse the repository at this point in the history
  • Loading branch information
pp449 authored Feb 12, 2024
2 parents 75b3b4a + e4b1af2 commit 2671e0b
Show file tree
Hide file tree
Showing 12 changed files with 383 additions and 46 deletions.
3 changes: 3 additions & 0 deletions src/@types/styles/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@ export type IconKind =
| 'warning'
| 'account'
| 'language'
| 'keyboard'
| 'exclamation'
| 'bell'
| 'myLocation'
| 'location';
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Header from '@components/Header';
import Announcement from '@pages/Announcement';
import FAQPage from '@pages/FAQ';
import Home from '@pages/Home';
import KeywordSubscribe from '@pages/KeywordSubscribe';
import MajorDecision from '@pages/MajorDecision';
import MapPage from '@pages/Map';
import My from '@pages/My';
Expand All @@ -24,6 +25,7 @@ const App = () => {
<Route path="/announcement/*" element={<Announcement />} />
<Route path="/major-decision/*" element={<MajorDecision />} />
<Route path="/my" element={<My />} />
<Route path="/keyword" element={<KeywordSubscribe />} />
<Route path="/tip/:type" element={<Tip />} />
<Route path="/FAQ" element={<FAQPage />} />
<Route path="/suggestion" element={<SuggestionPage />} />
Expand Down
40 changes: 40 additions & 0 deletions src/apis/subscribe/subscribeKeyword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import http from '@apis/http';

export const fetchSubscribeKeyword = async () => {
const userToken = localStorage.getItem('subscribe');
if (!userToken) return;

const res = await http.get<string[]>(
'/api/subscription/keyword?userToken=' + userToken,
);

return res.data;
};

export const postSubscribeKeyword = async (keyword: string) => {
const userToken = localStorage.getItem('subscribe');
if (!userToken) return;

const res = await http.post('/api/subscription/keyword', {
data: {
subscription: JSON.parse(userToken),
keyword,
},
});

return res;
};

export const deleteSubscribeKeyword = async (keyword: string) => {
const userToken = localStorage.getItem('subscribe');
if (!userToken) return;

const res = await http.delete('/api/subscription/keyword', {
data: {
subscription: JSON.parse(userToken),
keyword,
},
});

return res;
};
6 changes: 6 additions & 0 deletions src/components/Common/Icon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import {
MdOutlineKeyboardArrowDown,
MdAssignmentInd,
MdLanguage,
MdKeyboard,
MdError,
MdDoorbell,
MdOutlineLocationOn,
} from 'react-icons/md';

Expand Down Expand Up @@ -62,6 +65,9 @@ const ICON: { [key in IconKind]: IconType } = {
warning: MdOutlineError,
account: MdAssignmentInd,
language: MdLanguage,
keyboard: MdKeyboard,
exclamation: MdError,
bell: MdDoorbell,
myLocation: MdOutlineMyLocation,
location: MdOutlineLocationOn,
};
Expand Down
7 changes: 7 additions & 0 deletions src/constants/keyword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const KEYWORD_PAGE = {
TITLE: '키워드 알림 설정',
SUB_TITLE:
'키워드를 설정하시면 키워드가 포함된 공지사항이 올라올 때마다 알림을 보내드려요.',
PLACEHOLDER: '키워드를 입력해주세요. (예 : 장학금)',
REGISTERED_KEYWORD: '등록한 키워드',
};
5 changes: 5 additions & 0 deletions src/constants/toast-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ const TOAST_MESSAGES = {
OUT_OF_SHOOL: '학교 밖에서는 내 위치 정보를 제공하지 않아요!',
SHARE_LOCATION: '위치 정보를 공유해 주세요',
SEARCH_KEYWORD: '검색어를 입력해주세요',
NEED_SUBSCRIBE: '알림 설정을 먼저 해주세요',
NEED_MORE_TEXT: '2글자 이상 입력해주세요',
DUPLICATE_KEYWORD: '중복된 키워드가 설정되어있습니다',
EXCEED_SUBSCRIBE_MAX_COUNT: '키워드는 최대 5개까지 설정 가능합니다',
ERROR_MESSAGE: '일시적인 에러발생',
} as const;

export default TOAST_MESSAGES;
19 changes: 19 additions & 0 deletions src/hooks/useInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ChangeEvent, useCallback, useState } from 'react';

const useInput = () => {
const [inputValue, setInputValue] = useState<string>('');

const handleValue = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;

setInputValue(value);
}, []);

const resetValue = useCallback(() => {
setInputValue('');
}, []);

return [inputValue, handleValue, resetValue] as const;
};

export default useInput;
15 changes: 13 additions & 2 deletions src/mocks/handlers/subscribeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@ import { SERVER_URL } from '@config/index';
import { RequestHandler, rest } from 'msw';

export const subscribeHandler: RequestHandler[] = [
rest.post(SERVER_URL + '/api/subscription', (req, res, ctx) => {
rest.post(SERVER_URL + '/api/subscription/major', (req, res, ctx) => {
return res(ctx.status(200));
}),
rest.delete(SERVER_URL + '/api/subscription', (req, res, ctx) => {
rest.delete(SERVER_URL + '/api/subscription/major', (req, res, ctx) => {
return res(ctx.status(200));
}),
rest.get(SERVER_URL + '/api/subscription/keyword', (req, res, ctx) => {
const mockData = ['장학', '등록', '네이버'];

return res(ctx.status(200), ctx.json(mockData));
}),
rest.post(SERVER_URL + '/api/subscription/keyword', (req, res, ctx) => {
return res(ctx.status(200));
}),
rest.delete(SERVER_URL + '/api/subscription/keyword', (req, res, ctx) => {
return res(ctx.status(200));
}),
];
62 changes: 62 additions & 0 deletions src/pages/KeywordSubscribe/components/RegisteredKeywordList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { KEYWORD_PAGE } from '@constants/keyword';
import styled from '@emotion/styled';
import { THEME } from '@styles/ThemeProvider/theme';

interface RegisteredKeywordListProps {
keywords: string[];
deleteKeyword: (keyword: string) => void;
}

const RegisteredKeywordList = ({
keywords,
deleteKeyword,
}: RegisteredKeywordListProps) => {
return (
<Container>
{KEYWORD_PAGE.REGISTERED_KEYWORD}
{keywords && (
<KeywordContainer>
{keywords.map((keyword, index) => (
<KeywordWrapper key={index}>
{keyword}
<KeywordCancel onClick={() => deleteKeyword(keyword)}>
X
</KeywordCancel>
</KeywordWrapper>
))}
</KeywordContainer>
)}
</Container>
);
};

export default RegisteredKeywordList;

const Container = styled.div`
display: flex;
flex-direction: column;
padding: 5% 20px 0 20px;
`;

const KeywordContainer = styled.div`
display: flex;
padding-top: 10px;
flex-wrap: wrap;
`;

const KeywordWrapper = styled.div`
padding: 7px 7px 7px 14px;
border: 1px solid ${THEME.TEXT.GRAY};
border-radius: 15px;
margin: 5px;
display: flex;
align-items: center;
font-size: 0.9rem;
box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px, rgba(0, 0, 0, 0.24) 0px 1px 2px;
`;

const KeywordCancel = styled.button`
background-color: transparent;
border: none;
line-height: 1px;
`;
143 changes: 143 additions & 0 deletions src/pages/KeywordSubscribe/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
deleteSubscribeKeyword,
fetchSubscribeKeyword,
postSubscribeKeyword,
} from '@apis/subscribe/subscribeKeyword';
import Button from '@components/Common/Button';
import InformUpperLayout from '@components/InformUpperLayout';
import { KEYWORD_PAGE } from '@constants/keyword';
import TOAST_MESSAGES from '@constants/toast-message';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import useInput from '@hooks/useInput';
import useToasts from '@hooks/useToast';
import RegisteredKeywordList from '@pages/KeywordSubscribe/components/RegisteredKeywordList';
import { THEME } from '@styles/ThemeProvider/theme';
import { FormEvent, FormEventHandler, useEffect, useState } from 'react';

const KeywordSubscribe = () => {
const [keywords, setKeywords] = useState<string[]>([]);
const [inputKeyword, setInputKeyword, resetKeywords] = useInput();
const { addToast } = useToasts();

const fetchKeywords = async () => {
const data = await fetchSubscribeKeyword();
if (data) setKeywords(data);
};

const deleteKeyword = async (keyword: string) => {
const res = await deleteSubscribeKeyword(keyword);

if (res?.status !== 200) {
addToast(TOAST_MESSAGES.ERROR_MESSAGE);
return;
}

setKeywords((prevKeyword) => prevKeyword.filter((key) => key !== keyword));
};

const handleSubmit: FormEventHandler<HTMLFormElement> = (
e: FormEvent<HTMLFormElement>,
) => {
e.preventDefault();
onClickSubmit();
};

const onClickSubmit = async () => {
if (keywords.length > 4) {
addToast(TOAST_MESSAGES.EXCEED_SUBSCRIBE_MAX_COUNT);
resetKeywords();
return;
}

if (inputKeyword.length < 2) {
addToast(TOAST_MESSAGES.NEED_MORE_TEXT);
resetKeywords();
return;
}

if (keywords.includes(inputKeyword)) {
addToast(TOAST_MESSAGES.DUPLICATE_KEYWORD);
resetKeywords();
return;
}

const res = await postSubscribeKeyword(inputKeyword);
if (res && res.status === 200) {
setKeywords((prevKeywords) => [...prevKeywords, inputKeyword]);
resetKeywords();
return;
}

addToast(TOAST_MESSAGES.ERROR_MESSAGE);
};

useEffect(() => {
fetchKeywords();
}, []);

const checkIsAvailable = () => inputKeyword.length > 1;

return (
<>
<InformUpperLayout>
<InformUpperLayout.InformTitle title={KEYWORD_PAGE.TITLE} />
<InformUpperLayout.InformSubTitle subTitle={KEYWORD_PAGE.SUB_TITLE} />
</InformUpperLayout>

<InputWrapper onSubmit={handleSubmit}>
<KeywordInput
onChange={setInputKeyword}
placeholder={KEYWORD_PAGE.PLACEHOLDER}
maxLength={15}
value={inputKeyword}
/>
<Button
disabled={!checkIsAvailable()}
css={css`
position: absolute;
top: 10px;
right: 10%;
border: none;
width: auto;
height: auto;
background-color: transparent !important;
color: ${checkIsAvailable() ? THEME.PRIMARY : THEME.TEXT.GRAY};
padding: 0;
margin: 0;
&: hover {
cursor: pointer;
}
`}
>
등록
</Button>
</InputWrapper>

<RegisteredKeywordList
keywords={keywords}
deleteKeyword={deleteKeyword}
/>
</>
);
};

export default KeywordSubscribe;

const InputWrapper = styled.form`
padding: 0 20px 0 20px;
position: relative;
`;

const KeywordInput = styled.input`
width: 90%;
border: none;
border-bottom: 1.5px solid;
padding: 10px;
&:focus {
border: none;
outline: none;
border-bottom: 1.5px solid;
}
`;
6 changes: 5 additions & 1 deletion src/pages/My/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Modal from '@components/Common/Modal';
import MajorProvider from '@components/Providers/MajorProvider';
import ModalsProvider from '@components/Providers/ModalsProvider';
import ToastsProvider from '@components/Providers/ToastsProvider';
import { MODAL_BUTTON_MESSAGE, MODAL_MESSAGE } from '@constants/modal-messages';
import useModals from '@hooks/useModals';
import { render, screen } from '@testing-library/react';
Expand Down Expand Up @@ -64,14 +65,17 @@ describe('마이 페이지 동작 테스트', () => {
render(
<MajorProvider>
<ModalsProvider>
<My />
<ToastsProvider>
<My />
</ToastsProvider>
</ModalsProvider>
</MajorProvider>,
{ wrapper: MemoryRouter },
);

const majorEditButton = screen.getByText('학과 선택하러가기');
await userEvent.click(majorEditButton);

expect(mockRouterTo).toHaveBeenCalledWith('/major-decision');
});
});
Loading

0 comments on commit 2671e0b

Please sign in to comment.