Closed SEMINSEMINSEMIN closed 1 year ago
if ('visualViewport' in window) { const VIEWPORT_VS_CLIENT_HEIGHT_RATIO = 0.75; window.visualViewport.addEventListener('resize', function (event) { if ( (event.target.height * event.target.scale) / window.screen.height < VIEWPORT_VS_CLIENT_HEIGHT_RATIO ) console.log('keyboard is shown'); else console.log('keyboard is hidden'); }); }
Branch issue-338 created for issue: [FEED] 프로필 이미지 아이콘 텍스트에리어 길어질 경우 위치 이상함 + 포커스 상태로 리사이즈 경우 데스크탑에서도 bottom 50%
import { useState, useRef, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import TopBar from "../../components/TopBar";
import useAuth from "../../hook/useAuth";
import { PostEditWrapper } from "../../components/postEditWrapper.style";
import { ProductImgSetCont } from "../../components/ProductImageSet/productImageSet.style";
import { ImgUploadIcon } from "../../components/ImageUpload/imageUpload.style";
import { UserProfileImg } from "../../components/postEditUserProfile.style";
import Textarea from "../../components/Textarea/Textarea";
import { Contentimg } from "../../components/postEditContentImg.style";
import basicImg from "../../assets/basic-profile-img.png";
import deleteIcon from "../../assets/icon/icon-delete.png";
import useWindowSizeCustom from "../../hook/windowSize";
import NavBar from "../../components/NavBar/NavBar";
import Toast from "../../components/Toast";
let fileUrls = [];
export default function UploadPost() {
const [isBtnDisable, setIsBtnDisable] = useState(true);
const [showImages, setShowImages] = useState([]);
const [isFocused, setIsFocused] = useState();
const imagePre = useRef(null);
const textarea = useRef();
const fileInpRef = useRef(null);
const navigate = useNavigate();
const data = useAuth();
const toastRef = useRef(null);
const fileLabelRef =useRef();
// 화면 사이즈 변경 훅
const { width } = useWindowSizeCustom();
// 뒤로 가기, 또는 페이지 전환시 혹시라도 남아있을 fileURL, fileInpRef.current.value 제거 위해
useEffect(() => {
fileInpRef.current.value = null;
fileUrls = [];
}, []);
useEffect(() => {
const handleResize = (e) => {
if (window.innerWidth < 768) {
console.log(`window.innerHeight: ${window.innerHeight}`);
console.log(`window.visualViewport: ${e.target.height}`);
if (window.innerHeight !== e.target.height) {
fileLabelRef.current.style.bottom = "50%";
} else {
fileLabelRef.current.style.bottom = "5.51%";
}
// if (isFocused) {
// fileLabelRef.current.style.bottom = "50%";
// } else {
// fileLabelRef.current.style.bottom = "8.51%";
// }
}
};
window.visualViewport.addEventListener("resize", handleResize)
return () => window.visualViewport.removeEventListener("resize", handleResize);
}, [])
const handleFocus = () => {
setIsFocused(true);
}
const handleBlur = () => {
setIsFocused(false);
}
// textarea 자동 높이 조절
const handleTextarea = (e) => {
textarea.current.style.height = "auto";
textarea.current.style.height = textarea.current.scrollHeight + "px";
if (e.target.value.length === 0 && showImages.length === 0) {
setIsBtnDisable(true);
} else {
setIsBtnDisable(false);
}
// 글자 수 제한 테스트 중입니다.
// let text = e.target.value;
// let text_length = text.length;
// setContentText(text);
// let max_length = 100;
// if (text_length > max_length) {
// text = text.substring(0, 100);
// alert(100 + "자 이상 작성할 수 없습니다.");
// }
};
// const [visualViewport, setVisualViewport] = useState();
// 이미지 미리보기
let previewUrl = [];
const handleAddImages = (event) => {
if (
fileUrls.length +
fileInpRef.current.files.length <=
3
) {
const imageFiles = [...fileInpRef.current.files];
fileUrls.push(...imageFiles);
for (let i = 0; i < fileUrls.length; i++) {
let file = fileUrls[i];
const fileReader = new FileReader();
fileReader.onload = () => {
previewUrl.push(fileReader.result);
setShowImages([...previewUrl]);
};
fileReader.readAsDataURL(file);
}
setIsBtnDisable(false);
fileInpRef.current.value = null;
} else {
alert("이미지는 3개까지 업로드 할 수 있습니다.");
}
};
// 이미지 미리보기 삭제
const handleDeleteImage = (id) => {
setShowImages(showImages.filter((_, index) => index !== id));
if (!textarea.current.value && showImages.length === 1) {
setIsBtnDisable(true);
};
fileInpRef.current.value = null;
fileUrls = fileUrls.filter((_, index) => index !== id);
};
// const postImgName = [];
// 이미지 서버에 전송
const uploadImg = async (file) => {
const formData = new FormData();
formData.append("image", file);
try {
const res = await fetch(
"https://mandarin.api.weniv.co.kr/image/uploadfiles",
{
method: "POST",
body: formData,
}
);
const json = await res.json();
console.log(json);
const postImgName = json[0].filename;
return postImgName;
} catch (error) {
console.error(error);
}
};
// 저장 버튼 클릭 시 텍스트, 이미지 값 서버에 전송. 이미지는 서버에 있는 데이터를 가져와서 전송.
const CreatePost = async function (e) {
e.preventDefault();
const url = "https://mandarin.api.weniv.co.kr/post";
const imgUrls = [];
try {
for (const file of fileUrls) {
imgUrls.push(
"https://mandarin.api.weniv.co.kr/" +
(await uploadImg(file))
);
}
const productData = {
post: {
content: textarea.current.value,
image: imgUrls.join(","),
},
};
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: localStorage.getItem("Authorization"),
"Content-type": "application/json",
},
body: JSON.stringify(productData),
});
const json = await response.json();
console.log(json);
console.log("게시글 등록 완료");
// 게시글이 없다면 오류 alert
if (json.message) {
alert(json.message);
} else {
// 게시글 등록 성공하면 본인 프로필 페이지로 이동
handleShowToast();
setTimeout(function(){
navigate(`/account/profile/${json.post.author.accountname}`);
}, 1000)
}
} catch (error) {
console.error(error);
}
};
const handleShowToast = () => {
toastRef.current.style.transform = "scale(1)";
setTimeout(function(){
toastRef.current.style.transform = "scale(0)";
}, 3000)
return;
}
return (
<>
<TopBar
type="A4"
right4Ctrl={{ form: "postUpload", isDisabled: isBtnDisable }}
/>
<Toast ref={toastRef} msg="게시글이 업로드 되었습니다!"/>
<PostEditWrapper>
<UserProfileImg
src={data ? data.image : basicImg}
alt="게시글 작성자 프로필 사진"
/>
<form
style={{ flexBasis: "304px", height: "100%"}}
action=""
id={"postUpload"}
onSubmit={CreatePost}
>
<ProductImgSetCont htmlFor="productImg">
{/* <ExReport/> */}
<Textarea
placeholder="게시글 입력하기..."
onChange={handleTextarea}
ref={textarea}
onFocus={handleFocus}
onBlur={handleBlur}
rows={1}
/>
{/* 이미지 표시하는게 label 안에 있어도 되나? */}
{showImages.map((image, id) => (
<div className="each-image-cont" key={id}>
<Contentimg
src={image}
alt={`${image}-${id}`}
ref={imagePre}
/>
<button
className="delete-btn"
type="button"
onClick={() => handleDeleteImage(id)}
>
<img src={deleteIcon} alt="이미지 삭제" />
</button>
</div>
))}
</ProductImgSetCont>
<ImgUploadIcon className={"orange small location "} ref={fileLabelRef}>
<span className="ir">이미지 첨부</span>
<input
multiple
className="ir"
ref={fileInpRef}
type="file"
accept="image/jpg, image/gif, image/png, image/jpeg, image/bmp, image/tif, image/heic"
onChange={handleAddImages}
/>
</ImgUploadIcon>
</form>
</PostEditWrapper>
{width >= 768 ? <NavBar /> : <></>}
</>
);
}
VisualViewport resize시 텍스트에리어의 높이를 조정하는 방법을 썼을 때 아이콘은 잘 움직이긴 하는데 이제 긓 입력하고 다시 수정하려고 클릭하면 페이지 전체가 올라가버림
그리고 이 방법의 단점은 스크롤이 두 개 생긴다는 거
아니면 키보드 업일때 position static으로 하기...???
문서에 스크롤이 생길 경우 스크롤 탑을 0으로
동작은 하긴 하는데 유저 입장에서 불편할거 같다(왜냐하면 텍스트에리어 외의 다른 영역 스크롤시 스크롤 탑이 자꾸 바뀌니까)
import { useState, useRef, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import TopBar from "../../components/TopBar";
import useAuth from "../../hook/useAuth";
import { PostEditWrapper } from "../../components/postEditWrapper.style";
import { ProductImgSetCont } from "../../components/ProductImageSet/productImageSet.style";
import { ImgUploadIcon } from "../../components/ImageUpload/imageUpload.style";
import { UserProfileImg } from "../../components/postEditUserProfile.style";
import Textarea from "../../components/Textarea/Textarea";
import { Contentimg } from "../../components/postEditContentImg.style";
import basicImg from "../../assets/basic-profile-img.png";
import deleteIcon from "../../assets/icon/icon-delete.png";
import useWindowSizeCustom from "../../hook/windowSize";
import NavBar from "../../components/NavBar/NavBar";
import Toast from "../../components/Toast";
let fileUrls = [];
export default function UploadPost() {
const [isBtnDisable, setIsBtnDisable] = useState(true);
const [showImages, setShowImages] = useState([]);
const [isFocused, setIsFocused] = useState();
const [visualVh, setVisualVh] = useState();
const imagePre = useRef(null);
const textarea = useRef();
const fileInpRef = useRef(null);
const navigate = useNavigate();
const data = useAuth();
const toastRef = useRef(null);
const fileLabelRef =useRef();
// 화면 사이즈 변경 훅
const { width } = useWindowSizeCustom();
// 뒤로 가기, 또는 페이지 전환시 혹시라도 남아있을 fileURL, fileInpRef.current.value 제거 위해
useEffect(() => {
fileInpRef.current.value = null;
fileUrls = [];
}, []);
useEffect(() => {
const handleResize = (e) => {
// if (window.innerWidth < 768) {
// if (window.innerHeight !== e.target.height) {
// fileLabelRef.current.style.bottom = "50%";
// } else {
// fileLabelRef.current.style.bottom = "5.51%";
// }
// // if (isFocused) {
// // fileLabelRef.current.style.bottom = "50%";
// // } else {
// // fileLabelRef.current.style.bottom = "8.51%";
// // }
// }
setVisualVh(e.target.height);
};
window.visualViewport.addEventListener("resize", handleResize)
return () => window.visualViewport.removeEventListener("resize", handleResize);
}, [])
const handleFocus = (e) => {
setIsFocused(true);
// setIsFocused(true);
// const end = textarea.current.value.length;
// textarea.setSelectionRange(end, end);
// textarea.focus();
// e.target.scrollTop = e.target.scollHeight;
}
useEffect(() => {
const scrollHandler = () => {
console.log(document.documentElement.scrollTop);
document.documentElement.scrollTop = 0;
};
document.addEventListener("scroll", scrollHandler);
return () => document.removeEventListener("resize", scrollHandler);
}, []);
const handleBlur = () => {
setIsFocused(false);
}
// textarea 자동 높이 조절
const handleTextarea = (e) => {
// textarea.current.style.height = "auto";
// textarea.current.style.height = textarea.current.scrollHeight + "px";
// console.log(window.visualViewport);
// console.log(textarea.current.style.height);
if (e.target.value.length === 0 && showImages.length === 0) {
setIsBtnDisable(true);
} else {
setIsBtnDisable(false);
}
// 글자 수 제한 테스트 중입니다.
// let text = e.target.value;
// let text_length = text.length;
// setContentText(text);
// let max_length = 100;
// if (text_length > max_length) {
// text = text.substring(0, 100);
// alert(100 + "자 이상 작성할 수 없습니다.");
// }
};
// const [visualViewport, setVisualViewport] = useState();
// 이미지 미리보기
let previewUrl = [];
const handleAddImages = (event) => {
if (
fileUrls.length +
fileInpRef.current.files.length <=
3
) {
const imageFiles = [...fileInpRef.current.files];
fileUrls.push(...imageFiles);
for (let i = 0; i < fileUrls.length; i++) {
let file = fileUrls[i];
const fileReader = new FileReader();
fileReader.onload = () => {
previewUrl.push(fileReader.result);
setShowImages([...previewUrl]);
};
fileReader.readAsDataURL(file);
}
setIsBtnDisable(false);
fileInpRef.current.value = null;
} else {
alert("이미지는 3개까지 업로드 할 수 있습니다.");
}
};
// 이미지 미리보기 삭제
const handleDeleteImage = (id) => {
setShowImages(showImages.filter((_, index) => index !== id));
if (!textarea.current.value && showImages.length === 1) {
setIsBtnDisable(true);
};
fileInpRef.current.value = null;
fileUrls = fileUrls.filter((_, index) => index !== id);
};
// const postImgName = [];
// 이미지 서버에 전송
const uploadImg = async (file) => {
const formData = new FormData();
formData.append("image", file);
try {
const res = await fetch(
"https://mandarin.api.weniv.co.kr/image/uploadfiles",
{
method: "POST",
body: formData,
}
);
const json = await res.json();
console.log(json);
const postImgName = json[0].filename;
return postImgName;
} catch (error) {
console.error(error);
}
};
// 저장 버튼 클릭 시 텍스트, 이미지 값 서버에 전송. 이미지는 서버에 있는 데이터를 가져와서 전송.
const CreatePost = async function (e) {
e.preventDefault();
const url = "https://mandarin.api.weniv.co.kr/post";
const imgUrls = [];
try {
for (const file of fileUrls) {
imgUrls.push(
"https://mandarin.api.weniv.co.kr/" +
(await uploadImg(file))
);
}
const productData = {
post: {
content: textarea.current.value,
image: imgUrls.join(","),
},
};
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: localStorage.getItem("Authorization"),
"Content-type": "application/json",
},
body: JSON.stringify(productData),
});
const json = await response.json();
console.log(json);
console.log("게시글 등록 완료");
// 게시글이 없다면 오류 alert
if (json.message) {
alert(json.message);
} else {
// 게시글 등록 성공하면 본인 프로필 페이지로 이동
handleShowToast();
setTimeout(function(){
navigate(`/account/profile/${json.post.author.accountname}`);
}, 1000)
}
} catch (error) {
console.error(error);
}
};
const handleShowToast = () => {
toastRef.current.style.transform = "scale(1)";
setTimeout(function(){
toastRef.current.style.transform = "scale(0)";
}, 3000)
return;
}
return (
<>
<TopBar
type="A4"
right4Ctrl={{ form: "postUpload", isDisabled: isBtnDisable }}
/>
<Toast ref={toastRef} msg="게시글이 업로드 되었습니다!"/>
<PostEditWrapper visualVh={visualVh ? visualVh + "px" : "100vh"}>
<UserProfileImg
src={data ? data.image : basicImg}
alt="게시글 작성자 프로필 사진"
/>
<form
style={{ flexBasis: "304px", height: "100%"}}
action=""
id={"postUpload"}
onSubmit={CreatePost}
>
<ProductImgSetCont htmlFor="productImg">
{/* <ExReport/> */}
<Textarea
placeholder="게시글 입력하기..."
onChange={handleTextarea}
ref={textarea}
onFocus={handleFocus}
onBlur={handleBlur}
rows={1}
/>
{/* 이미지 표시하는게 label 안에 있어도 되나? */}
{showImages.map((image, id) => (
<div className="each-image-cont" key={id}>
<Contentimg
src={image}
alt={`${image}-${id}`}
ref={imagePre}
/>
<button
className="delete-btn"
type="button"
onClick={() => handleDeleteImage(id)}
>
<img src={deleteIcon} alt="이미지 삭제" />
</button>
</div>
))}
</ProductImgSetCont>
<ImgUploadIcon className={"orange small location "} ref={fileLabelRef}>
<span className="ir">이미지 첨부</span>
<input
multiple
className="ir"
ref={fileInpRef}
type="file"
accept="image/jpg, image/gif, image/png, image/jpeg, image/bmp, image/tif, image/heic"
onChange={handleAddImages}
/>
</ImgUploadIcon>
</form>
</PostEditWrapper>
{width >= 768 ? <NavBar /> : <></>}
</>
);
}
import { useState, useRef, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import TopBar from "../../components/TopBar";
import useAuth from "../../hook/useAuth";
import { PostEditWrapper } from "../../components/postEditWrapper.style";
import { ProductImgSetCont } from "../../components/ProductImageSet/productImageSet.style";
import { ImgUploadIcon } from "../../components/ImageUpload/imageUpload.style";
import { UserProfileImg } from "../../components/postEditUserProfile.style";
import Textarea from "../../components/Textarea/Textarea";
import { Contentimg } from "../../components/postEditContentImg.style";
import basicImg from "../../assets/basic-profile-img.png";
import deleteIcon from "../../assets/icon/icon-delete.png";
import useWindowSizeCustom from "../../hook/windowSize";
import NavBar from "../../components/NavBar/NavBar";
import Toast from "../../components/Toast";
let fileUrls = [];
export default function UploadPost() {
const [isBtnDisable, setIsBtnDisable] = useState(true);
const [showImages, setShowImages] = useState([]);
const [isFocused, setIsFocused] = useState();
const [visualVh, setVisualVh] = useState();
const imagePre = useRef(null);
const textarea = useRef();
const fileInpRef = useRef(null);
const navigate = useNavigate();
const data = useAuth();
const toastRef = useRef(null);
const fileLabelRef =useRef();
// 화면 사이즈 변경 훅
const { width } = useWindowSizeCustom();
// 뒤로 가기, 또는 페이지 전환시 혹시라도 남아있을 fileURL, fileInpRef.current.value 제거 위해
useEffect(() => {
fileInpRef.current.value = null;
fileUrls = [];
}, []);
useEffect(() => {
const handleResize = (e) => {
// if (window.innerWidth < 768) {
// if (window.innerHeight !== e.target.height) {
// fileLabelRef.current.style.bottom = "50%";
// } else {
// fileLabelRef.current.style.bottom = "5.51%";
// }
// // if (isFocused) {
// // fileLabelRef.current.style.bottom = "50%";
// // } else {
// // fileLabelRef.current.style.bottom = "8.51%";
// // }
// }
setVisualVh(e.target.height);
};
window.visualViewport.addEventListener("resize", handleResize)
return () => window.visualViewport.removeEventListener("resize", handleResize);
}, [])
const handleFocus = (e) => {
setIsFocused(true);
// setIsFocused(true);
// const end = textarea.current.value.length;
// textarea.setSelectionRange(end, end);
// textarea.focus();
// e.target.scrollTop = e.target.scollHeight;
}
useEffect(() => {
const scrollHandler = () => {
if (window.visualViewport.height < window.innerHeight) {
console.log(document.documentElement.scrollTop);
document.documentElement.scrollTop = 0;
}
};
document.addEventListener("scroll", scrollHandler);
return () => document.removeEventListener("resize", scrollHandler);
}, []);
const handleBlur = () => {
setIsFocused(false);
}
// textarea 자동 높이 조절
const handleTextarea = (e) => {
// textarea.current.style.height = "auto";
// textarea.current.style.height = textarea.current.scrollHeight + "px";
// console.log(window.visualViewport);
// console.log(textarea.current.style.height);
if (e.target.value.length === 0 && showImages.length === 0) {
setIsBtnDisable(true);
} else {
setIsBtnDisable(false);
}
// 글자 수 제한 테스트 중입니다.
// let text = e.target.value;
// let text_length = text.length;
// setContentText(text);
// let max_length = 100;
// if (text_length > max_length) {
// text = text.substring(0, 100);
// alert(100 + "자 이상 작성할 수 없습니다.");
// }
};
// const [visualViewport, setVisualViewport] = useState();
// 이미지 미리보기
let previewUrl = [];
const handleAddImages = (event) => {
if (
fileUrls.length +
fileInpRef.current.files.length <=
3
) {
const imageFiles = [...fileInpRef.current.files];
fileUrls.push(...imageFiles);
for (let i = 0; i < fileUrls.length; i++) {
let file = fileUrls[i];
const fileReader = new FileReader();
fileReader.onload = () => {
previewUrl.push(fileReader.result);
setShowImages([...previewUrl]);
};
fileReader.readAsDataURL(file);
}
setIsBtnDisable(false);
fileInpRef.current.value = null;
} else {
alert("이미지는 3개까지 업로드 할 수 있습니다.");
}
};
// 이미지 미리보기 삭제
const handleDeleteImage = (id) => {
setShowImages(showImages.filter((_, index) => index !== id));
if (!textarea.current.value && showImages.length === 1) {
setIsBtnDisable(true);
};
fileInpRef.current.value = null;
fileUrls = fileUrls.filter((_, index) => index !== id);
};
// const postImgName = [];
// 이미지 서버에 전송
const uploadImg = async (file) => {
const formData = new FormData();
formData.append("image", file);
try {
const res = await fetch(
"https://mandarin.api.weniv.co.kr/image/uploadfiles",
{
method: "POST",
body: formData,
}
);
const json = await res.json();
console.log(json);
const postImgName = json[0].filename;
return postImgName;
} catch (error) {
console.error(error);
}
};
// 저장 버튼 클릭 시 텍스트, 이미지 값 서버에 전송. 이미지는 서버에 있는 데이터를 가져와서 전송.
const CreatePost = async function (e) {
e.preventDefault();
const url = "https://mandarin.api.weniv.co.kr/post";
const imgUrls = [];
try {
for (const file of fileUrls) {
imgUrls.push(
"https://mandarin.api.weniv.co.kr/" +
(await uploadImg(file))
);
}
const productData = {
post: {
content: textarea.current.value,
image: imgUrls.join(","),
},
};
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: localStorage.getItem("Authorization"),
"Content-type": "application/json",
},
body: JSON.stringify(productData),
});
const json = await response.json();
console.log(json);
console.log("게시글 등록 완료");
// 게시글이 없다면 오류 alert
if (json.message) {
alert(json.message);
} else {
// 게시글 등록 성공하면 본인 프로필 페이지로 이동
handleShowToast();
setTimeout(function(){
navigate(`/account/profile/${json.post.author.accountname}`);
}, 1000)
}
} catch (error) {
console.error(error);
}
};
const handleShowToast = () => {
toastRef.current.style.transform = "scale(1)";
setTimeout(function(){
toastRef.current.style.transform = "scale(0)";
}, 3000)
return;
}
return (
<>
<TopBar
type="A4"
right4Ctrl={{ form: "postUpload", isDisabled: isBtnDisable }}
/>
<Toast ref={toastRef} msg="게시글이 업로드 되었습니다!"/>
<PostEditWrapper visualVh={visualVh ? visualVh + "px" : "100vh"}>
<UserProfileImg
src={data ? data.image : basicImg}
alt="게시글 작성자 프로필 사진"
/>
<form
style={{ flexBasis: "304px", height: "100%"}}
action=""
id={"postUpload"}
onSubmit={CreatePost}
>
<ProductImgSetCont htmlFor="productImg">
{/* <ExReport/> */}
<Textarea
placeholder="게시글 입력하기..."
onChange={handleTextarea}
ref={textarea}
onFocus={handleFocus}
onBlur={handleBlur}
rows={1}
/>
{/* 이미지 표시하는게 label 안에 있어도 되나? */}
{showImages.map((image, id) => (
<div className="each-image-cont" key={id}>
<Contentimg
src={image}
alt={`${image}-${id}`}
ref={imagePre}
/>
<button
className="delete-btn"
type="button"
onClick={() => handleDeleteImage(id)}
>
<img src={deleteIcon} alt="이미지 삭제" />
</button>
</div>
))}
</ProductImgSetCont>
<ImgUploadIcon className={"orange small location "} ref={fileLabelRef}>
<span className="ir">이미지 첨부</span>
<input
multiple
className="ir"
ref={fileInpRef}
type="file"
accept="image/jpg, image/gif, image/png, image/jpeg, image/bmp, image/tif, image/heic"
onChange={handleAddImages}
/>
</ImgUploadIcon>
</form>
</PostEditWrapper>
{width >= 768 ? <NavBar /> : <></>}
</>
);
}
https://user-images.githubusercontent.com/104843477/210065526-fbdd37bd-de38-415d-9d11-f86a8af6e1a2.MOV
라이브러리 사용하고 싶다..