23/12/14 심화팀과제 리팩토링(2)
2023. 12. 15. 08:58ㆍ카테고리 없음
<WriteNewFIx.jsx> 어제하다만 파일을 봅시당
그전에
<firebase.js> 파이어베이스 기본설정을 보자
import {initializeApp} from 'firebase/app'; //전체적으로 파이어베이스 초기설정하기
import {getAuth} from 'firebase/auth';
import {getFirestore} from '@firebase/firestore';
import {getStorage} from 'firebase/storage'; //각각 authenication firestore storaged 에서 데이터
//받는 함수const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID,
};
//환경변수로 만들어 놓은 파일에서 prcess.env. 라는 방식으로// Initialize Firebase //환경변수로 선언한 것을 가져옴
const app = initializeApp(firebaseConfig); //초기화하나봄
export const auth = getAuth(app); //각각 익스포트해주고
export default app;
export const db = getFirestore(app);
export const storage = getStorage(app);
<WriteNewFIx.jsx>
import React, {useState} from 'react';
import styled from 'styled-components';
import {auth, db, storage} from '../../shared/firebase'; //얘는 우리가만든 파이어베이스파일이고
import {getDownloadURL, ref, uploadBytes} from 'firebase/storage';
import {addDoc, collection} from 'firebase/firestore'; //스토리지와 파이어스토어 함수
import {closeAddModal} from '../../redux/modules/modalSlice'; //모달슬라이스 여기서도 쓰시는군
import {useDispatch} from 'react-redux';
import {toast} from 'react-toastify'; //토스트가 좋죠
import {useNavigate} from 'react-router-dom';
import pinImg from '../../asset/pin.png';
import {showPublicModal} from '../../redux/modules/publicModalSlice';
import {addList} from '../../redux/modules/fixList'; //얘는 리스트 추가하는 리듀서
import bonobono from '../../asset/bonobono.jpg';
import {Map, MapMarker} from 'react-kakao-maps-sdk'; //이번 프로젝트에 카카오맵썼는데
import useKakaoLoader from '../useKaKaoLoader'; //그때 필요한 함수인가봄
function WriteNewFix() {
useKakaoLoader();
//지도
const [latitude, seLatitude] = useState(33.450701); //위도
const [longitude, setLongitude] = useState(126.570667); //경도
const [buildingName, setBuildingName] = useState(''); //경도
const searchAddress = () => {
// Kakao Maps에서 제공하는 주소 검색 대화상자 열기
if (window.daum && window.daum.Postcode) { //조건문에 두개가 있다면
new window.daum.Postcode({
oncomplete: function (addrData) { //키 갖고 벨류로 펑션이라...
const geocoder = new window.kakao.maps.services.Geocoder(); //그냥 지오코드 만드는식같고
// 주소로 상세 정보를 검색
geocoder.addressSearch(addrData.address, function (result, status) {
// 정상적으로 검색이 완료됐으면
if (status === window.kakao.maps.services.Status.OK) {
//첫번째 결과의 값을 활용
// 해당 주소에 대한 좌표를 받아서
const currentPos = new window.kakao.maps.LatLng(result[0].y, result[0].x);
seLatitude(currentPos.Ma);
setLongitude(currentPos.La); //여긴 알겠네 받아온 위도 경도를 스테이트에 담네
// 최종 주소 변수-> 주소 정보를 해당 필드에 넣는다.
// 선택한 주소로 입력 필드 업데이트
setAddrInput(addrData.address); //아하 얘는 밑에 있구먼
setBuildingName(addrData.buildingName); //얘는 위에 있고
}
});
},
}).open();
} else {
alert('카카오map 로드가 안됨');
}
};
const dispatch = useDispatch();
const navigate = useNavigate();
const [selectedFile, setSelectedFile] = useState(''); //유즈스테이트 많네
const [previewFile, setPreviewFile] = useState('');
const {email, displayName, uid, photoURL} = auth.currentUser; //아까 익스포트한 auth에서
const [addrInput, setAddrInput] = useState(''); //유저정보 구조분해로 가져오고
const [formState, setFormState] = useState({
title: '',
content: '',
});
const {title, content} = formState;
//공용 함수
const onChangeHandler = event => { //온체인지 핸들러 공용으로 잘 만드셨네
const {name, value} = event.target;
setFormState(prev => ({...prev, [name]: value})); //키값도 그때 그때 다를테고
}; //난 익숙하지 않은데 여기서 익혀야징
//파일 삭제
const handleFileDelete = event => {
setPreviewFile(''); //여기에 리턴이 필요한건가?
return;
};
//파일 선택
const handleFileSelect = event => {
setSelectedFile(event.target.files[0]); //파일 그자체와 url화 시킨
setPreviewFile(URL.createObjectURL(event.target.files[0])); //파일을 별도의 스테이트 보관 흐음
};
//이미지 파일 업로드
const handleUpload = async () => {
//[파일선택] 버튼 안눌러서 선택한 파일 없는경우
if (selectedFile === '') {
return ''; // selectedFile 요거 없으면 실행 한시키는 함수
}
//밑에 로직은 동일한 uid에 따른 이미지를 참조하는듯 const imageRef = ref(storage, `${auth.currentUser.uid}/${selectedFile.name}`);
//const imageRef = ref(storage, `${userUid}/${selectedFile.name}`);
try {
await uploadBytes(imageRef, selectedFile);
return await getDownloadURL(imageRef); //비동기 함수 두개 실행하고
} catch (error) {
throw error; //에러발생 처리
}
};
let formattedDate = new Intl.DateTimeFormat('ko-KR', {
dateStyle: 'full',
timeStyle: 'short',
}).format(new Date()); //날짜 만드는 함수가 이런것도 있구낭
let cancelBtn = () => {
dispatch( //퍼블릭 모달을 스테이트 없데이트해서
showPublicModal({ //아래 벨류값을 사용하는 어제 말한 리듀서
isUse: true,
title: '😯 정말 나가시겠어요?',
message: '저장하지 않은 내용은 사라져요.',
btnMsg: '계속 작성',
btnType: 'continue',
btnMsg2: '나가기',
btnType2: 'exit', // 함수 대신 타입 지정
}),
);
//dispatch(closeAddModal()); // 새글작성모달 닫기
navigate('/'); //홈으로 돌아가고
};
const formOnSubmit = async event => {
event.preventDefault(); //디폴트 막고
try {
//1. 이미지 파일 업로드
const uploadImageUrl = await handleUpload(); //위에서 만든 요함수 실행 ㅇㅋ
//2. 모달창에 입력된 새로운 데이터
const newData = { //새로추가할 리스트 정보
title,
content,
date: formattedDate,
createdAt: new Date(), //파이어베이스에 업로드는 저 함수 실행으로
image_url: uploadImageUrl ? uploadImageUrl : bonobono,
uid, //이미 했고 변수명으로 스토어 바꾸기 위한
displayName, //뉴데이터를 만드신듯하다
email,
photoURL: photoURL ? photoURL : pinImg,
addrInput,
latitude,
longitude,
buildingName, //오우 종류가 많다.
};
//3. 파이어스토어에 데이터 저장 // 말그대로의 데이터저장
const collectionRef = collection(db, 'fixs');
const res = await addDoc(collectionRef, newData);
//4. 모달닫기
dispatch(addList({...newData, id: res.id})); //일단 리스트 자체에 추가하는데
dispatch(closeAddModal()); //파이어 글 자체의 아이디를 한번 담아준다.
toast.success('저장되었습니다.'); //파이어베이스는 아이디를 부여하므로
} catch (Error) {}
};
return (
<>
<form onSubmit={formOnSubmit}>
<ScDiv>
<h1>어디로 '픽스' 할까요?</h1>
<div>
<ScInputTitle
name="title"
value={title}
onChange={onChangeHandler} //아까 공용화한 온체인지 핸들러
placeholder="제목을 입력해주세요." // 네임을 타이틀로 주고 이걸 키값으로 갖는
maxLength={30} //객체로 만든다.
required //리콰이얼드가 필수사항이라던데 흐음
></ScInputTitle>
</div>
<div>
<ScTextareaContent
name="content"
placeholder="내용을 입력해주세요"
value={content}
onChange={onChangeHandler} //여기도 똑같이 온체인지
required
></ScTextareaContent>
</div>
{!previewFile && ( //프리뷰 파일이 없으면 뜨는 창
<ScDivFileUpload>
<input type="file" name="selectedFile" id="fileAttach" onChange={handleFileSelect}></input>
<label htmlFor="fileAttach">사진 선택</label>
</ScDivFileUpload>
)}
{previewFile && (
<>
<ScDivPreview>
<img name="previewFile" size="large" src={previewFile} />
<ScButtonDelete type="button" id="fileDelete" onClick={handleFileDelete}></ScButtonDelete>
<label htmlFor="fileDelete">사진 삭제</label> //위에 아이디를 지정한건
</ScDivPreview> // htmlFor 요거 쓰실려고 한거구먼
</> //응용 머선 129 //잘한다...
)}
{/* 맵 바꾸기 */}
<ScDivMapSearch>
<div required onClick={searchAddress}> //요거 위에서 해석 어려웠던 카카오맵주소검색함수
<input
required
id="addr"
placeholder=" 📍 장소 검색" //악 귀여워
value={addrInput}
onChange={event => setAddrInput(event.target.value)}
/> //그냥 인풋창
<button type="button">장소 검색</button>
</div>
<Map center={{lat: latitude, lng: longitude}} style={{width: '100%', height: '230px'}}>
<MapMarker
key={`${latitude}-${longitude}`}
position={{lat: latitude, lng: longitude}}
image={{
src: 'https://velog.velcdn.com/images/jetiiin/post/6eff67e2-349b-4fe4-854f-12d1e384536a/image.png', // 마커이미지의 주소입니다
size: {
width: 64,
height: 69,
}, // 마커이미지의 크기입니다
options: {
offset: {
x: 27,
y: 69,
}, // 마커이미지의 옵션입니다. 마커의 좌표와 일치시킬 이미지 안에서의 좌표를 설정합니다.
},
}}
></MapMarker> //뭐 그냥 보여주는 부분
</Map>
</ScDivMapSearch>
<ScDivButton>
<ScButtonFix type="submit">Fix하기</ScButtonFix> // 여기에 서브밋 타입이 있네?
<ScButtonFix type="button" onClick={cancelBtn}> //위에 태그에 폼태그로 스타일드 컴포넌트화
취소 //하셨나보넹
</ScButtonFix>
</ScDivButton>
</ScDiv>
</form>
</>
);
}
const ScDiv = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 7px;
margin: 10px auto;
& h1 {
font-size: 25px;
margin: 15px auto;
text-align: center;
font-weight: 600;
}
& div {
width: 100%;
padding-right: 20px;
padding-left: 30px;
display: flex;
gap: 20px;
align-items: center;
}
& img {
object-fit: cover;
width: 200px;
height: 150px;
}
`;
const ScDivMapSearch = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
margin: 0;
padding-left: 0;
& div {
padding: 0;
}
& button {
display: none;
}
& input {
width: 100%;
border: 1px solid var(--deep-blue);
border-radius: 8px;
cursor: pointer;
height: 30px;
&:hover {
border: 1px solid var(--deep-blue);
box-shadow: rgba(57, 167, 255, 0.4) 0px 0px 0px 3px;
}
}
`;
const ScDivFileUpload = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
padding-left: 10px;
& input {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
& label {
border: 1px solid var(--deep-blue);
background-color: #fff;
color: var(--deep-blue);
border-radius: 8px;
padding: 6px 14px;
font-weight: 500;
font-size: 14px;
outline: none;
cursor: pointer;
&:hover {
border: 1px solid var(--deep-blue);
box-shadow: rgba(57, 167, 255, 0.4) 0px 0px 0px 3px;
}
}
`;
const ScDivButton = styled.div`
display: flex;
justify-content: center;
gap: 10px;
`;
const ScButtonDelete = styled.button`
display: none;
`;
const ScDivPreview = styled.div`
display: flex;
justify-content: center;
flex-direction: column;
& label {
border: 1px solid var(--deep-blue);
background-color: #fff;
color: var(--deep-blue);
border-radius: 8px;
padding: 6px 14px;
font-weight: 500;
font-size: 14px;
outline: none;
cursor: pointer;
&:hover {
border: 1px solid var(--deep-blue);
box-shadow: rgba(57, 167, 255, 0.4) 0px 0px 0px 3px;
}
}
`;
const ScInputTitle = styled.input`
width: 100%;
outline: none;
font-size: 19px;
margin-top: 8px;
margin-bottom: 8px;
//padding-bottom: 10px;
border: none;
font-weight: 500;
border: 1px solid var(--deep-blue);
border-radius: 8px;
//background-color: var(--light-blue);
//padding: 20px auto;
padding: 10px;
&::placeholder {
color: #bbb;
}
`;
const ScTextareaContent = styled.textarea`
min-height: 14vh;
max-height: 30vh;
overflow-y: auto;
box-sizing: content-box;
outline: none;
line-height: 1.6em;
margin-bottom: 5px;
font-size: 15px;
word-break: keep-all;
border: none;
resize: none;
width: 100%;
padding-top: 10px;
color: var(--black);
//background-color: var(--light-blue);
border: 1px solid var(--deep-blue);
border-radius: 8px;
//padding-left: 13px;
padding: 10px;
&::placeholder {
color: #bbb;
}
`;
const ScButtonFix = styled.button`
width: 20%;
height: 34px;
margin-top: 10px;
font-weight: 600;
border-radius: 8px;
font-size: 15px;
background-color: var(--deep-blue);
color: white;
&:hover {
border: 1px solid var(--deep-blue);
box-shadow: rgb(57, 167, 255, 0.4) 0px 0px 0px 3px;
cursor: pointer;
}
border: none;
`;
export default WriteNewFix;
// css도 봐야하는데 일단 여기까징