체크 버튼을 클릭시, 모달이 나온다.
//TodoContent.tsx
export default function TodoContent({ content, id, fetchData }: Props) {
const [isOpenModal, setIsOpenModal] = useState(false);
const clickModal = () => {
setIsOpenModal(true);
};
const closeModal = () => {
setIsOpenModal(false);
};
return (
~~~
<div className="todo-content-wrapper">
<button
onClick={() => {
clickModal();
}}
>
<Icon.CheckCircle size={18} color="white" />
</button>
</div>
~~~
{isOpenModal && (
<Modal
variant="certification"
onSubmit={onSubmit}
closeModal={closeModal}
/>
)}
)
}
//Modal.tsx
import React, { ChangeEvent, FormEvent, useRef, useState } from "react";
import Button from "./Button";
import Text from "./Text";
import * as Icon from "react-feather";
import { updateTodo } from "../../services/api/todo";
type Variant = "code" | "certification";
interface Props {
variant?: Variant;
closeModal: () => void;
onSubmit: any;
}
interface UpdateParams {
key: "authenticationMethod" | "authenticationContent";
value: string;
}
export default function Modal({ variant, closeModal, onSubmit }: Props) {
const inputRef = useRef(null);
const [newDone, setNewDone] = useState<TodoParam>({
complitedAt: new Date(),
authenticationMethod: "",
authenticationContent: "",
});
const [isShowImage, SetIsShowImage] = useState(false);
const update = (params: UpdateParams[]) => {
const _newDone = { ...newDone };
params.forEach((p) => {
_newDone[p.key] = p.value;
});
setNewDone(_newDone);
};
return (
<div className="background" onClick={closeModal}>
<div className="modal-container" onClick={(e) => e.stopPropagation()}>
{variant === "code" ? (
<div className="variant-code">
<Text type="title">입장코드를 입력해주세요</Text>
<input placeholder="e.g. 1234567" />
</div>
) : (
<form className="variant-certification">
<Text type="title">공부내용 인증</Text>
<Text type="title" size="sm">
블로그 인증
</Text>
<input
onChange={(e) => {
update([
{
key: "authenticationContent",
value: e.currentTarget.value,
},
{
key: "authenticationMethod",
value: "link",
},
]);
}}
placeholder="블로그 링크를 입력해주세요"
/>
<Text type="title" size="sm">
스크린샷 인증
</Text>
{isShowImage ? (
<img
src={newDone.authenticationContent}
alt={newDone.authenticationContent}
/>
) : (
<>
<div
className="input-file-box"
onClick={() => {
(inputRef.current as any).click();
}}
>
<Icon.UploadCloud size={18} color="#828fa3" />
<Text size="md" color="gray" style={{ marginLeft: "5px" }}>
파일 업로드
</Text>
</div>
<input
className="hidden"
ref={inputRef}
type="file"
style={{ visibility: "hidden" }}
name={"fileName"}
onChange={(e) => {
e.target.files &&
update([
{ key: "authenticationMethod", value: "image" },
{
key: "authenticationContent",
value: e.target.files[0].name,
},
]);
SetIsShowImage(true);
}}
/>
</>
)}
</form>
)}
<div className="button-wrapper">
<Button onClick={closeModal} size="sm" variant="outlined">
취소
</Button>
<Button onClick={(e: React.MouseEvent<HTMLButtonElement>) => onSubmit(e, newDone)} size="sm">
확인
</Button>
</div>
</div>
</div>
);
}
먼저 모달컴포넌트는 type Variant = "code" | "certification"
에 따라 2가지 다른 모달을 보여준다.
현재 하고 있는 형태는 인증과 관련된 certification Modal 부분을 구현할 것이다.
newDone 의 상태를 보면 객체를 가지고있다. complitedAt, authenticationMethod, authenticationContent 의 상태를 동시에 관리하기 위해서!
update 함수를 보자면 먼저 newDone의 값을 복사해오고, param을 key, value의 객체로 받아오는데, 이 받아온 객체를 forEach로 순회하며 복사하여 만든 _newDone의 key와 value에 해당하는것을 매핑해주고, setNewDone으로 newDone을 업데이트 해주게된다.
_newDone으로 한번 더 복사해서 사용한 이유는 ? 🤔 _newDone
으로 update함수 내에서만 사용될 변수를 만들어줘서 newDone의 값을 복사해와서 key와 value에 따라서 _newDone을 만들어준뒤, setNewDone에 _newDone을 넣어 newDone의 상태를 변경해주었다.
newDone은 state이기에 newDone 자체의 값을 변경해 줄 때에는 setState로만 해줘야하니까!
spread 연산자를 이용하면 깊은복사일까 얕은복사일까
currentTarget
vs target
?
currentTarget
: 이벤트가 발생한 요소의 부모 요소target
: 이벤트가 발생한 요소예를 들어, 버튼을 클릭하면 currentTarget
은 버튼의 부모 요소인 div이고 target
은 버튼 자체입니다.
background에 onClick으로 closeModal을 해주었는데, 모달창내에 input이나 다른 요소를 클릭하더라도 closeModal이 적용되는 이벤트 버블링 현상이 발생하였다. -> 좀 더 자세히 알아보기!!!
막기위해 e.stopPropagation()을 사용
.background {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 0;
@include flexbox();
}
.modal-container {
@include column-flexbox();
gap: 20px;
background-color: $grayDark;
padding: 20px;
width: 100%;
max-width: 440px;
.variant-certification {
@include column-flexbox(start, start);
width: 100%;
gap: 20px;
input {
border: 1px solid $gray;
}
.input-file-box {
@include flexbox();
background-color: $background;
width: 100%;
padding: 0 16px;
height: 40px;
border: 1px solid $background;
border-radius: 4px;
box-sizing: border-box;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.hidden {
position: fixed;
}
}
.button-wrapper {
@include flexbox();
gap: 10px;
width: 100%;
}
}