KCMF 프로젝트의 관리자 페이지에서 공통으로 쓰이는 키워드를 관리하는 페이지를 작업했다. 주제, 장르, 오디오로 나뉘고 해당 분류에 따라 쓰이는 키워드를 추가하거나 삭제할 수 있는 기능이며, 해당 키워드들은 전역적으로 다른 페이지에도 적용이 된다.
...
const MediaForm = ({ data }: MediaFormProps) => {
const { getMediaList } = useMedia();
const { updateMedia } = useMediaMutation();
const { mediaId, mediaType, keywordList, updateDt } = data;
const [currentCodeValueList, setCurrentCodeValueList] = useState<Array<Code>>(
[...keywordList].reverse(),
);
const [addedCodeValueList, setAddedCodeValueList] = useState<Array<string>>([]);
const [removedCodeIdList, setRemovedCodeIdList] = useState<Array<number>>([]);
...
const {
register,
handleSubmit,
watch,
reset,
formState: { errors },
} = useForm({
mode: 'onChange',
defaultValues: {
keyword: '',
},
});
const watchedKeyword: string = watch('keyword');
const onSubmit = () => {
addKeyword(watchedKeyword);
reset();
};
const handleMediaUpload = async () => {
try {
const payload: UpdateMediaRequestType = {
codeValueList: addedCodeValueList,
isRemoveCodeIdList: removedCodeIdList,
};
await updateMedia(mediaId, payload);
openAlert('confirmSuccess');
await getMediaList();
} catch (error) {
console.log(error);
}
};
const deleteKeyword = useCallback((id: number, keyword: string) => {
if (id) {
setRemovedCodeIdList((prev) => {
if (prev.includes(id)) return prev;
return [...prev, id];
});
setCurrentCodeValueList((prev) => prev.filter((item) => item.codeId !== id));
} else {
// 프론트단에서 추가한 커스텀 키워드일 경우
setAddedCodeValueList((prev) => prev.filter((item) => item !== keyword));
setCurrentCodeValueList((prev) => prev.filter((item) => item.codeValue !== keyword));
}
}, []);
const addKeyword = useCallback(
(keyword: string) => {
if (currentCodeValueList.some((item) => item.codeValue === keyword)) {
// 이미 존재하는 코드일 때
openAlert('duplicateKeyword');
return;
} else {
setAddedCodeValueList((prev) => {
if (prev.includes(keyword)) return prev;
return [...prev, keyword];
});
const keywordIdNull = {
codeId: null,
codeName: mediaType,
codeValue: keyword,
description: mediaTypeMap[mediaType],
};
setCurrentCodeValueList((prev) => [keywordIdNull, ...prev]);
}
},
[currentCodeValueList, mediaType, mediaTypeMap, openAlert],
);
// 커스텀으로 추가된 요소의 경우
// 추가: setCurrentCodeValueList 에 id 를 null로 가진 요소에 포함시켜 추가
// 커스텀 요소 제거시 => id 가 null 이며, value 가 '단편영화' 일 경우 제거
return (
<>
<Box className="flex justify-between w-full">
...
</Box>
<ContentTable mtNone>
<colgroup>
<col className="w-2/12" />
<col className="w-2/12" />
</colgroup>
<TableBody>
<TableRow>
<ContentTableCell
header
required
className="text-center"
rowSpan={currentCodeValueList?.length + 1}
>
{mediaTypeMap[mediaType]} {mediaType === 'CATEGORY' ? '관리' : '장르'}
</ContentTableCell>
<ContentTableCell colSpan={2} className="border-t-1">
<Box className="flex gap-8">
<form noValidate onSubmit={handleSubmit(onSubmit)} className="w-[300px]">
<TextField
{...register('keyword')}
error={!!errors.keyword}
helperText={errors.keyword?.message as string}
fullWidth
placeholder="키워드를 입력 후, Enter를 눌러주세요."
className="max-w-400"
/>
</form>
<Button
onClick={() => {
addKeyword(watchedKeyword);
reset();
}}
color="black"
>
추가
</Button>
</Box>
</ContentTableCell>
</TableRow>
{currentCodeValueList?.map((keyword, i) => (
<TableRow key={i}>
<ContentTableCell className="text-center" header secondary>
{currentCodeValueList?.length - i}
</ContentTableCell>
<ContentTableCell className="flex" colSpan={3}>
<Typography
className="self-center text-center flex-1"
variant="body1"
color="black"
>
{keyword?.codeValue}
</Typography>
<Button
onClick={() => deleteKeyword(keyword?.codeId, keyword?.codeValue)}
variant="outlined"
color="error"
size="small"
>
삭제
</Button>
</ContentTableCell>
</TableRow>
))}
</TableBody>
</ContentTable>
...
</>
);
};
export default MediaForm;
이미 존재하는 키워드의 경우 현재 입력한 키워드와 codeValue 값을 비교해 경고창을 띄우고 리셋하도록 설정했다.
결과:
주제를 추가 후 로그를 찍었더니 아래와 같이 나타난다.
키워드 수정 후 저장을 눌러 handleMediaUpload
를 실행하면, 최종적으로 제거된 키워드의 id 값과 프론트에서 추가된 키워드 배열값을 백엔드 요청값으로 보내게 된다. 현재 추가된 키워드와 기존 데이터로 내려오는 키워드를 분간하여 state 로 분리하는게 조금 복잡했지만 꽤나 재미있는 작업이었다.