요구사항
마이 타입을 설정하고 갱신해야한다.
체크된건 주황색/안된건 하얀색이다.
1. 서버로 부터 전체 마이타입을 가져온다. [{id:number,name:string,thumbnail:string(imgURL)},...]형식이다.
2. 만약에 유저가 이미 설정한 마이타입이 있다면 체크돼있어야한다.
3. 사진을 누르면 체크 상태가 바뀐다.
4. 3개보다 더 많이 체크되면 안된다.
5. 초기화 누르면 초기상태가 된다.
6. 확인하기를 누르면 서버로 요청을 보내고 갱신한다. [id1,id2,id3,...]형식이다.
문제 해결 전략
모달에서 상태관리는 복잡하다. useContext로 서버의 전체 마이타입과 모달을 닫는 setIsOpen을 뿌려준다.
useFieldArray을 고려해볼 수 있다. 하지만 문서를 읽어봤고 들어오는 데이터를 생각해봤다.
해당 훅의 사용은 오히려 개발경험과 코드구조에 악영향을 줄 것 같았다.
전체 마이 타입은 거의 바뀔 데이터도 아니고(사실 고정된 데이터라 생각한다.) 폼을 입력하는 중엔 큰 동적인 변화가 없는 것도 이유이다. 따라서 useForm의 기능을 활용해보고자 했다.
그래서 폼을 다음과 같이 설정한다. {[key:number]:boolean} (key는 마이타입의 아이디고 value는 체크됐는지 여부이다.)
각각의 마이 타입별로 필드를 관리하자고 생각했다.
타입 에러는 타입 캐스팅을 활용하면 해결할 수 있으나, 추후엔 유지보수성에 악영향이 갈 수도 있다.
따라서 상황을 보고 useFieldArray를 고려해 볼 것이다.
선택인지 아닌지가 중요하므로 input의 타입을 checkbox로 활용했다. 이를 숨기고 내 ui와 연결할 것이다.
1. useQuery를 이용해 전체 마이타입과 유저가 설정한 마이타입을 가져온다.
2. useForm의 defaultValues를 설정해준다.
export const getDefaultValues = (
all: IfCategory[],
my: IfUserType[],
): FormCategoryDataType => {
const result:FormCategoryDataType = {} //결과
const myIdArr = my.map(e => e.id) //현재 내 타입의 id들을 뽑아준다.
all.forEach(e => {
result[e.id] = myIdArr.includes(e.id) // 전체타입과 내타입을 비교한다.
})
return result
}
const {data: curType} = useContext(TypeContext)
const methods = useForm<FormCategoryDataType>({
defaultValues: getDefaultValues(allType, curType),
})
const {onSubmit} = usePostCategory()
return (
<>
<Bar>마이 타입</Bar>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<CardContainer data={allType}/>
<ButtonContainer/>
</form>
</FormProvider>
</>
)
getDefaultValues()로 초기값을 설정해준다.
3.필드 만들기
return (
<Container>
{data.map(e => (
<TypeCard key={e.id} data={e} />
))}
</Container>
)
전체 타입을 기준으로 카드를 뿌려준다. 각 카드가 필드가 될 것이다.
const TypeCard = ({data}: proptype) => {
const id = data.id
const {category, control, getValues} = useCategoryForms(id)
const flag = useWatch({
control: control,
name: String(id) as never,
}) as boolean
const checkRef = useRef<HTMLInputElement | null>(null)
const blockClick = (): boolean => {
const allData = getValues()
const cnt = Object.values(allData).filter(e => e === true).length
if (cnt >= 3) {
return false
}
return true
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
category.field.onChange(!e.target.checked)
}
return (
<Container
onClick={() => {
if (checkRef.current) {
if (!flag && !blockClick()) {
alert('타입은 최대 3개까지 설정 가능합니다.')
} else {
checkRef.current.click()
console.log(id, category.field, flag)
}
}
}}
>
<input
type="checkbox"
hidden
id={String(id)}
name={category.field.name}
ref={e => {
category.field.ref(e)
checkRef.current = e
}}
onChange={handleChange}
/>
<TypeImage src={data.thumbnail} alt={data.name} />
{<CustomCheck flag={flag} />}
<Title>{data.name}</Title>
</Container>
)
}
3번을 구현하기 위해 필요한 것은 다음과 같다.
3-1. 현재 체크 상태를 가져와야한다.
3-2. 체크가 바뀌면 리랜더링이 돼야한다.
따라서 getValues()가 아닌 useWatch()로 상태를 가져와 리랜더링을 발생시켜 ui를 바꿔준다.
const flag = useWatch({
control: control,
name: String(id) as never,
}) as boolean
3-3. 사진을 누르면 체크 상태가 바뀌어야한다.
따라서 input field와 전체 카드 영역을 링크시켜줘야한다.
const TypeCard = ({data}: proptype) => {
const checkRef = useRef<HTMLInputElement | null>(null) // input의 ref를 활용할 수 있다.
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
category.field.onChange(!e.target.checked)
} // checkbox인풋의 값을 제어할 수 있다.
return (
<Container
onClick={() => {
if (checkRef.current) {
checkRef.current.click() // hidden인 input을 클릭한다.
}
}
}}
>
<input
type="checkbox"
hidden // 가려져 있다.
id={String(id)}
name={category.field.name}
ref={e => {
category.field.ref(e)
checkRef.current = e // field의 ref를 checkRef에 할당한다.
}}
onChange={handleChange} // checkRef.current.click()
/>
</Container>
)
}
field.ref로 input에 연결 가능하다. 이걸 checkRef에 연결시켜준다.input이 hidden이여도, checkRef.click()을한다면, checkbox input이 click되는 효과를 얻을 수 있다.
4. 3개이상이면 더 체크 못하게 하기
const blockClick = (): boolean => {
const allData = getValues()
const cnt = Object.values(allData).filter(e => e === true).length
if (cnt >= 3) {
return false
}
return true
}
전체 폼 데이터를 가져와야하므로 getValues()를 활용한다.
막는건 굳이 리랜더링을 발생시킬 필요가 없다. alert를 띄울 것이기 때문이다.
<Container
onClick={() => {
if (checkRef.current) {
if (!flag && !blockClick()) {
alert('타입은 최대 3개까지 설정 가능합니다.')
} else {
checkRef.current.click()
}
}
}}
>
만약에 3개가 체크된 상태인데 체크가 안된걸 체크하려고 한다면(새로운걸 선택하려고 한다면) 막아버린다.
5. reset()
const ButtonContainer = () => {
const {reset} = useFormContext<FormCategoryDataType>()
return (
<Container>
<ModalButton mark="sub" variant="contained" onClick={()=>{reset()}}>
초기화
</ModalButton>
<ModalButton variant="contained" type="submit">
확인하기
</ModalButton>
</Container>
)
}
reset은 폼 상태를 defaultValue로 만들어준다.
만약에 아무것도 선택 안한 상태로 만들고 싶다면 전체 폼 데이터를 가져오고 그걸 모두 false시킨 결과를 넣어줄 것이다.
6. 서버로 보내기
const {setIsOpen} = useContext(TypeContext)
const myTypeMutation = useMutation((d: IfUserTypePost) => postMyType(d), {
onSuccess: () => {
queryClient.invalidateQueries() // 상태를 바꾸면 강제로 내 타입을 refetch해야한다.
alert('타입 추가 및 변경 성공')
setIsOpen(false) //모달을 닫아준다.
},
onError: () => {
alert('타입 추가 및 변경 실패')
setIsOpen(false)
},
useErrorBoundary: false,
})
const onSubmit = (data: FormCategoryDataType) => {
const result = Object.keys(data)
.filter(e => data[e] === true)
.map(e => Number(e))
// 서버로 보내는 양식에 맞게 폼 데이터를 가공한다.
myTypeMutation.mutate({categoryIds: result})
}
서버로 보내는 양식을 보면 useFieldArray가 맞나 싶긴한데 저 데이터 가공이 딱히 어려운게 아니긴 하다
'개발' 카테고리의 다른 글
bcrypt invalid ELF header 에러 해결하기 (0) | 2024.02.14 |
---|---|
github oauth2 클라이언트와 서버 구현해보기 -1 (0) | 2024.02.12 |
프로젝트에서 예상치 못한 곳에서 발생하는 리다이렉트를 막은 방법 (0) | 2024.01.08 |
react-hook-form 폼 유효성 검사기능을 추가한 뒤 submit이 안될 때 (0) | 2024.01.07 |
반복되는 컴포넌트 key에 index를 넣으면 안되는 이유 (0) | 2024.01.03 |