-
Notifications
You must be signed in to change notification settings - Fork 1
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
Feat/#315: 키워드 구독 기능 추가 #319
Changes from 14 commits
0e29f69
8dbcb9f
c14b527
b7b7a55
93fe6e7
03bdb24
6a9300e
ea369af
25019fe
1f9bdf0
9123331
cbcdd2f
8964dcf
a62d7cd
d36a5aa
4013345
a087a64
dfc5928
6c34964
e4b1af2
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,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; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const KEYWORD_PAGE = { | ||
TITLE: '키워드 알림 설정', | ||
SUB_TITLE: | ||
'키워드를 설정하시면 키워드가 포함된 공지사항이 올라올 때마다 알림을 보내드려요.', | ||
PLACEHOLDER: '키워드를 입력해주세요. (예 : 장학금)', | ||
REGISTERED_KEYWORD: '등록한 키워드', | ||
}; |
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; |
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; | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { | ||
deleteSubscribeKeyword, | ||
fetchSubscribeKeyword, | ||
postSubscribeKeyword, | ||
} from '@apis/subscribe/subscribeKeyword'; | ||
import InformUpperLayout from '@components/InformUpperLayout'; | ||
import { KEYWORD_PAGE } from '@constants/keyword'; | ||
import TOAST_MESSAGES from '@constants/toast-message'; | ||
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 { KeyboardEventHandler, 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 handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (e) => { | ||
if (e.key !== 'Enter' || e.nativeEvent.isComposing) return; | ||
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 ? true : false); | ||
|
||
return ( | ||
<> | ||
<InformUpperLayout> | ||
<InformUpperLayout.InformTitle title={KEYWORD_PAGE.TITLE} /> | ||
<InformUpperLayout.InformSubTitle subTitle={KEYWORD_PAGE.SUB_TITLE} /> | ||
</InformUpperLayout> | ||
|
||
<InputWrapper> | ||
<KeywordInput | ||
onChange={setInputKeyword} | ||
placeholder={KEYWORD_PAGE.PLACEHOLDER} | ||
maxLength={15} | ||
onKeyDown={handleKeyDown} | ||
value={inputKeyword} | ||
/> | ||
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. form 이 더 깔끔한거 같네요 수정할게요 |
||
<KeywordSubmit | ||
disabled={!checkIsAvailable()} | ||
isAvailable={checkIsAvailable()} | ||
onClick={onClickSubmit} | ||
> | ||
등록 | ||
</KeywordSubmit> | ||
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. 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. 저도 고민이 많이 됐었는데, 일단은 |
||
</InputWrapper> | ||
|
||
<RegisteredKeywordList | ||
keywords={keywords} | ||
deleteKeyword={deleteKeyword} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
export default KeywordSubscribe; | ||
|
||
const InputWrapper = styled.div` | ||
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; | ||
} | ||
`; | ||
|
||
const KeywordSubmit = styled.button<{ isAvailable: boolean }>` | ||
color: ${(prop) => (prop.isAvailable ? THEME.PRIMARY : THEME.TEXT.GRAY)}; | ||
border: none; | ||
background-color: transparent; | ||
position: absolute; | ||
top: 10px; | ||
right: 10%; | ||
|
||
&: hover { | ||
cursor: pointer; | ||
} | ||
`; |
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.