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

[PUBLIC] SkillAutocomplete 무한 리렌더링 해결 #1115

Merged
merged 3 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/app/my-page/profile/panel/SkillsEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,16 @@ const SkillsEditor = ({
endIconButton={
<Tutorial title="스킬 추가 방법" content={<SkillsTutorial />} />
}
sx={{
width: '100%',
}}
>
<SkillAutocomplete
skillList={selected}
setSkillList={setSelected}
type="SKILL"
placeholder="스킬을 입력해주세요"
autocompleteSx={{ width: '100%' }}
/>
</FieldWithLabel>
</CuModal>
Expand Down
170 changes: 21 additions & 149 deletions src/components/SkillAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import useAxiosWithAuth from '@/api/config'
import useModal from '@/hook/useModal'
import { ISkill } from '@/types/IUserProfile'
import { getUniqueArray } from '@/utils/getUniqueArray'
import {
Autocomplete,
Button,
CircularProgress,
Stack,
SxProps,
TextField,
Typography,
} from '@mui/material'
import React, { useEffect, useState } from 'react'
import { Button, Stack, SxProps, Typography } from '@mui/material'
import React, { useState } from 'react'
import CuTextModal from './CuTextModal'
import TagChip from './TagChip'
import { ControllerRenderProps, UseFormTrigger } from 'react-hook-form'
import { convertNonAlphabeticToHex } from '@/utils/convertNonAlpbabetToHex'

const TIMEOUT = 500
import SkillComboBox from './skillAutocomplete/SkillComboBox'
import SkillList from './skillAutocomplete/SkillList'

// 리액트 훅 폼을 위한 리펙토링 필요
const SkillAutocomplete = ({
Expand All @@ -41,119 +29,24 @@ const SkillAutocomplete = ({
}) => {
const [tagList, setTagList] = useState(skillList) // 검색 된 데이터

const [text, setText] = useState('') // 검색 텍스트

const [timeOut, setTimeOut] = useState(TIMEOUT)
const [isLoading, setIsLoading] = useState(false)

const axiosWithAuth = useAxiosWithAuth()

const {
isOpen,
openModal: openAlertModal,
closeModal: closeAlertModal,
} = useModal()

useEffect(() => {
const countdown = setInterval(() => {
setTimeOut((prev) => prev - TIMEOUT / 5)
}, TIMEOUT / 5)

return () => clearInterval(countdown)
}, [timeOut])

useEffect(() => {
if (timeOut === 0 && text !== '' && isLoading) {
axiosWithAuth
.get(
`${
process.env.NEXT_PUBLIC_CSR_API
}/api/v1/skill/search?keyword=${convertNonAlphabeticToHex(text)}`,
)
.then((res) => {
setTagList((prev) => getUniqueArray(prev.concat(res.data), 'tagId'))
setIsLoading(false)
})
.catch(() => {
setIsLoading(false)
})
}
}, [timeOut])

const handleTextFieldChange = (e: any) => {
setText(e.target.value)
if (e.target.value === '') {
setIsLoading(false)
return
} else if (isLoading === false) {
setIsLoading(true)
}
setTimeOut(TIMEOUT)
}

const handleInput = (_: any, value: string[]) => {
if (value.length < skillList.length) {
return
}
const newSkillList: ISkill[] = []
value.map((newValue) => {
newSkillList.push(
tagList.find((skill) => newValue === skill.name) as ISkill,
)
})
setSkillList(newSkillList)
if (trigger) trigger('tagList')
}

return (
<>
<Autocomplete
{...field}
multiple
sx={{
fieldset: {
height: '2.5rem',
},
}}
disableClearable
loading={isLoading}
value={skillList.map((skill) => skill.name)}
inputValue={text}
options={tagList.map((tag) => tag.name)}
onChange={handleInput}
// onInputChange={handleTextFieldChange}
renderTags={() => <></>}
renderInput={(params) => (
<TextField
{...params}
disabled={skillList?.length >= 10}
onChange={handleTextFieldChange}
size="small"
placeholder={
placeholder ?? '프레임워크 또는 개발언어를 입력해주세요.'
}
sx={{ position: 'relative', maxWidth: '26rem', ...autocompleteSx }}
error={error}
InputProps={{
...params.InputProps,
endAdornment: (
<>
{isLoading ? (
<CircularProgress
color="primary"
size={'1.25rem'}
sx={{
position: 'absolute',
left: ['82.5%', '87.5%'],
}}
/>
) : null}
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
<SkillComboBox
skillList={skillList}
setSkillList={setSkillList}
tagList={tagList}
setTagList={setTagList}
field={field}
error={error}
trigger={trigger}
placeholder={placeholder}
autocompleteSx={autocompleteSx}
/>
<Stack
rowGap={['1rem', '0.5rem']}
Expand All @@ -162,7 +55,6 @@ const SkillAutocomplete = ({
flexWrap={'wrap'}
justifyContent={'flex-start'}
alignItems={'center'}
maxWidth={'26rem'}
>
<Stack
direction={'row'}
Expand All @@ -187,33 +79,13 @@ const SkillAutocomplete = ({
</Typography>
</Button>
</Stack>
{skillList.length ? (
skillList.map((skill) => {
const tag = tagList.find((tag) => tag.name === skill.name)
if (!tag) return null
return (
<TagChip
key={tag.tagId}
name={tag.name}
onDelete={() => {
const newTags = skillList.filter(
(curTag) => curTag.name !== tag.name,
)
setSkillList(newTags)
}}
color={tag.color}
/>
)
})
) : error ? (
<Typography variant="Caption" color={'error'}>
필수 항목입니다.
</Typography>
) : (
<Typography variant={'Caption'} color={'text.alternative'}>
선택된 {type === 'SKILL' ? '스킬이' : '태그가'} 없습니다.
</Typography>
)}
<SkillList
skillList={skillList}
setSkillList={setSkillList}
tagList={tagList}
type={type}
error={!!error}
/>
</Stack>
<CuTextModal
open={isOpen}
Expand Down
155 changes: 155 additions & 0 deletions src/components/skillAutocomplete/SkillComboBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { ISkill } from '@/types/IUserProfile'
import {
Autocomplete,
CircularProgress,
SxProps,
TextField,
} from '@mui/material'
import React, { useEffect, useState } from 'react'
import { ControllerRenderProps, UseFormTrigger } from 'react-hook-form'
import { convertNonAlphabeticToHex } from '@/utils/convertNonAlpbabetToHex'
import { getUniqueArray } from '@/utils/getUniqueArray'
import useAxiosWithAuth from '@/api/config'

const TIMEOUT = 500

const SkillComboBox = ({
skillList,
setSkillList,
tagList,
setTagList,
field,
error,
trigger,
placeholder,
autocompleteSx,
}: {
skillList: ISkill[]
setSkillList: (value: ISkill[]) => void
tagList: ISkill[]
setTagList: React.Dispatch<React.SetStateAction<ISkill[]>>
field?: ControllerRenderProps<any, 'tagList'>
error?: boolean
trigger?: UseFormTrigger<any>
placeholder?: string
autocompleteSx?: SxProps
}) => {
const [text, setText] = useState('') // 검색 텍스트

const [isLoading, setIsLoading] = useState(false)
const [timeout, setTimeout] = useState(TIMEOUT)

const axiosWithAuth = useAxiosWithAuth()

useEffect(() => {
const countdown = setInterval(() => {
setTimeout((prev) => {
if (prev === 0) {
return prev
} else {
return prev - TIMEOUT / 2
}
})
}, TIMEOUT / 2)

return () => clearInterval(countdown)
}, [])

useEffect(() => {
if (timeout === 0 && text !== '' && isLoading) {
axiosWithAuth
.get(
`${
process.env.NEXT_PUBLIC_CSR_API
}/api/v1/skill/search?keyword=${convertNonAlphabeticToHex(text)}`,
)
.then((res) => {
setTagList((prev) => getUniqueArray(prev.concat(res.data), 'tagId'))
setIsLoading(false)
})
.catch(() => {
setIsLoading(false)
})
.finally(() => {
setTimeout(TIMEOUT)
})
}
}, [timeout])

const handleTextFieldChange = (e: any) => {
setText(e.target.value)
if (e.target.value === '') {
setIsLoading(false)
return
} else if (isLoading === false) {
setIsLoading(true)
}
setTimeout(TIMEOUT)
}

const handleInput = (_: any, value: string[]) => {
if (value.length < skillList.length) {
return
}
const newSkillList: ISkill[] = []
value.map((newValue) => {
newSkillList.push(
tagList.find((skill) => newValue === skill.name) as ISkill,
)
})
setSkillList(newSkillList)
if (trigger) trigger('tagList')
}

return (
<Autocomplete
{...field}
multiple
sx={{
fieldset: {
height: '2.5rem',
},
}}
disableClearable
loading={isLoading}
value={skillList.map((skill) => skill.name)}
inputValue={text}
options={tagList.map((tag) => tag.name)}
onChange={handleInput}
renderTags={() => <></>}
renderInput={(params) => (
<TextField
{...params}
disabled={skillList?.length >= 10}
onChange={handleTextFieldChange}
size="small"
placeholder={
placeholder ?? '프레임워크 또는 개발언어를 입력해주세요.'
}
sx={{ position: 'relative', maxWidth: '26rem', ...autocompleteSx }}
error={error}
InputProps={{
...params.InputProps,
endAdornment: (
<>
{isLoading ? (
<CircularProgress
color="primary"
size={'1.25rem'}
sx={{
position: 'absolute',
left: ['82.5%', '87.5%'],
}}
/>
) : null}
{/* {params.InputProps.endAdornment} */}
</>
),
}}
/>
)}
/>
)
}

export default SkillComboBox
Loading
Loading