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도 봐야하는데 일단 여기까징