23/12/19 심화 리뷰(5)

2023. 12. 20. 08:47카테고리 없음

지원님의

<DetailPage.jsx> 메인페이지에서 리스트를 누르면 해당리스트의 상세페이지로 역할한다.

import {CancelButton, SubButton} from '../components/UI/Button';  //커스텀 버튼 다운
import {useNavigate, useParams} from 'react-router-dom';   // 리스트 아이디를 받아올 유즈파람스
import {auth} from '../shared/firebase';
import styled from 'styled-components';
import React from 'react';
import {useEffect} from 'react';
import {toast} from 'react-toastify';
import {useDispatch} from 'react-redux';
import {__deleteFix, __getFix} from '../redux/modules/DetailSlice';   //thunk 오우오우
import {useSelector} from 'react-redux';
import DetailMap from '../components/DetailMap';  // 디테일맵 컴포넌츠가 있으시네 이따 확인
import FadeLoader from 'react-spinners/FadeLoader';   // 요건 머지? 첨보는데?

function DetailPage() {
  // const [user, setUser] = useState;
  const {id} = useParams();                //유알엘에 아이디 가져오시고
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const {isLoading, isError, fix} = useSelector(state => state.fix);  //리스트 담겨있는 RTK에서
                                                                                                     //상태 가져오시고
  const navigateEditdetail = () => {
    navigate(`/editdetail/${id}`);                     //editdetail  페이지라는게 있으시군 
  };

  useEffect(() => {
    dispatch(__getFix(id));                     //지원님이 만드신 thunk도 확인해야겠다.
  }, []);

  const user = auth.currentUser;   //로그인한 유저정보 받아오고

  const deletePost = async post => {
    const deleteCheck = window.confirm('삭제하시겠습니까?');
    if (deleteCheck) {                                   // 컨펌을 통해 삭제 확인받고 해당 정보가 true이면
      await dispatch(__deleteFix(id));       //thunk 리듀서로 id값을 받아서 스테이트 변경하는구먼
      toast.success('삭제되었습니다');     //액션크리에이터들을 확인해야겠군
      navigate('/');                                      //완료되면 홈이동
    } else {
      return;           // 종료
    }
  };

  if (isLoading) {
    return (
      <ScLoding>
        <FadeLoader color="#3693d6" /  //아하 위에 정체불명의 페이드 로더구먼
      </ScLoding>                                      //라이브러리 같은데 이따 확인해야겠구먼
    );
  }

  if (isError) {
    toast.success('오류가 발생했습니다. 다시 시도해주세요'); //에러가 트루라면 홈이동 
    navigate('/');
  }

  return (
    <>
      <ScContainer>
        <ScMain>
          <ScTitleBox>
            <ScH1>{fix.title} </ScH1>
          </ScTitleBox>
          <ScImg src={fix.image_url}></ScImg>
          <ScP>{fix.content}</ScP>
          {}
          <DetailMap />
          <ScBtnBox>
            {user ? (
              user.email !== fix.email ? (
                <></>
              ) : (
                <>
                  <SubButton onClick={navigateEditdetail}>수정</SubButton>
                  <CancelButton onClick={() => deletePost()}>삭제</CancelButton>
                </>
              )
            ) : (
              <></>
            )}
          </ScBtnBox>
        </ScMain>
      </ScContainer>
    </>
  );
}

const ScContainer = styled.div`
  display: flex;
  justify-content: center;
  width: 100vw;
  height: 150vh;
`;

const ScMain = styled.div`
  width: 60%;
  height: 150vh;
  border: 2px solid #f6f6f6;
  border-radius: 5px;
`;

const ScImg = styled.img`
  height: 30%;
  width: 100%;
`;

const ScH1 = styled.h1`
  height: max-content;
  margin-left: 30px;
  font-size: 2rem;
  background: none;
  @media only screen and (min-width: 1500px) {
    font-size: 300%;
  }
`;

const ScTitleBox = styled.div`
  height: max-content;
  border-bottom: 2px solid var(--light-blue);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 25px;
`;

const ScP = styled.p`
  display: flex;
  align-items: center;
  margin: 50px 40px 0px 40px;
  font-size: 18px;
  @media only screen and (min-width: 1500px) {
    font-size: 150%;
  }
`;

const ScBtnBox = styled.div`
  width: 93%;
  height: 70px;
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  margin: 30px 50px 0px 0px;
  gap: 10px;
`;

const ScLoding = styled.div`
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
`;

export default DetailPage;

//일단 thunk가 궁금하니까 ㄱ

<DetailSlice.js>

import {createSlice, createAsyncThunk} from '@reduxjs/toolkit';  // thunk 를 위한 것들
import {getDoc, doc} from 'firebase/firestore';
import {db} from '../../shared/firebase';
import {deleteDoc} from 'firebase/firestore';    파이어베이스 데이터 삭제 툴

const initialState = {
  fix: {},                         //리스트 자체는 객체형 
  isLoading: false,
  isError: false,           //세가지 키와 벨류
  error: null,
};

export const __getFix = createAsyncThunk('getFix', async (payload, thunkAPI) => {
  try {
    const postRef = doc(db, 'fixs', payload);
    const post = await getDoc(postRef);            // 파이어 베이스에서 해당 포스트들 받아오고
    return thunkAPI.fulfillWithValue(post.data());   // 리턴에 post.data를 넘기지 않고 요값을 넘긴는 방법이 있군
  } catch (err) {
    return thunkAPI.rejectWithValue(err);    //에러시에 넘길 페이로드
  }
});

export const __deleteFix = createAsyncThunk('deleteFix', async (payload, thunkAPI) => {
  try {
    const removepost = await deleteDoc(doc(db, 'fixs', payload)); //파이어베이스의 해당 값을 지우고
    thunkAPI.dispatch(__getFix);                          //다시한번 변경된값으로 스테이트 변경
    return thunkAPI.fulfillWithValue(removepost);
  } catch (err) {
    return thunkAPI.rejectWithValue(err);                 //수정은 딴곳에 두셨나?
  }
});

export const FixSlice = createSlice({
  name: 'fix',
  initialState,
  reducers: {},
  extraReducers: builder => {                     //청크이니까 엑스트라 리듀서
    builder
      .addCase(__getFix.pending, (state, action) => {          //이제보니 builder 형태네 오오오
        state.isLoading = true;
        state.isError = false;                                       //pending이 정보 받는중일때
      })
      .addCase(__getFix.fulfilled, (state, action) => {
        state.isLoading = false;
        state.isError = false;                                                //fulfiiled가 정보 받았을때
        state.fix = action.payload;                                         //받은페이로드를 스테이트.fix 를 교체하시는군
      })
      .addCase(__getFix.rejected, (state, action) => {
        state.isLoading = false;
        state.isError = true;                                                 //rejected가 정보 못받았을때
        state.error = action.payload;
      });
    builder
      .addCase(__deleteFix.pending, (state, action) => {
        state.isLoading = true;
        state.isError = false;
      })
      .addCase(__deleteFix.fulfilled, (state, action) => {            // 어차피 위에서 getFIx한번 더쓰니까
        state.isLoading = false;                                            // 여기는 상태들만 바꿔주셨군 흠흠
        state.isError = false;
      })
      .addCase(__deleteFix.rejected, (state, action) => {
        state.isLoading = false;
        state.isError = true;
        state.error = action.payload;
      });
    // builder
    //   .addCase(__editFix.pending, (state, action) => {
    //     state.isLoading = true;
    //     state.isError = false;
    //   })
    //   .addCase(__editFix.fulfilled, (state, action) => {
    //     const editfix = state.fix.findIndex(fix => {
    //       return fix.id === action.payload.id;
    //     });
    //     state.fix.splice(editfix, 1, action.payload);
    //     // state.fix = action.payload;
    //     state.isLoading = false;
    //     state.isError = false;
    //   })
    //   .addCase(__editFix.rejected, (state, action) => {
    //     state.isLoading = false;
    //     state.isError = true;
    //     state.error = action.payload;
    //   });
  },
});

export default FixSlice.reducer;

 

<editDetail.jsx> 네이밍으로 보니 디테일페이지를 수정 하나봄

import styled from 'styled-components';
import {SubButton} from '../components/UI/Button';
import {useState} from 'react';
import {doc, getDoc, updateDoc} from 'firebase/firestore';
import {db} from '../shared/firebase';
import {useEffect} from 'react';
import {useNavigate, useParams} from 'react-router-dom';
import {storage} from '../shared/firebase';
import {ref} from 'firebase/storage';
import {getDownloadURL, uploadBytes} from 'firebase/storage';
import {toast} from 'react-toastify';
import {Map, MapMarker} from 'react-kakao-maps-sdk';

function EditDetailPage() {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [uploadImg, setUploadImg] = useState(null);
  const [detailPost, setDetailPost] = useState({});
  const [previewImg, setPreviewImg] = useState(null);
  const [addrInput, setAddrInput] = useState('');
  const [latitude, setLatitude] = useState(''); //위도
  const [longitude, setLongitude] = useState(''); //경도
  const [buildingName, setBuildingName] = useState(''); //빌딩네임             //유즈스테이트 많으시다.
  const {id} = useParams();
  const navigate = useNavigate();

  useEffect(() => {
    const getFix = async () => {
      const postRef = doc(db, 'fixs', id);
      const post = await getDoc(postRef);   파이어베이스에서 특정 아이디의 포스트만 가져오시고
      const postData = post.data();
      setTitle(postData.title);
      setContent(postData.content);                   //그포스트의 정보들을 전부 스테이트에 셋하시고
      setDetailPost(postData);
      setAddrInput(postData.addrInput);
      setPreviewImg(postData.image_url);
      setUploadImg(postData.image_url);
      setLatitude(postData.latitude);
      setLongitude(postData.longitude);
      setBuildingName(postData.buildingName);
    };
    getFix();
  }, []);

  console.log(detailPost);

  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);
              setLatitude(currentPos.Ma);
              setLongitude(currentPos.La);                               // 여기서 얻어온 경도 위도를 셋하는건 알겠구먼
              // 최종 주소 변수-> 주소 정보를 해당 필드에 넣는다.
              // 선택한 주소로 입력 필드 업데이트

              setAddrInput(addrData.address);
              setBuildingName(addrData.buildingName);        // 건물이름과 주소를 셋하시고
            }
          });
        },
      }).open();
    } else {
      alert('카카오map 로드가 안됨');
    }
  };
  const imgOnclickHandler = e => {
    setUploadImg(e.target.files[0]);                              // 이미지 자체를 담은거 하나
    setPreviewImg(URL.createObjectURL(e.target.files[0]));    //URL담는거 하나
  };

  const titleOnchangeHandler = e => {
    setTitle(e.target.value);
  };

  const contentOnchangeHandler = e => {
    setContent(e.target.value);
  };
  // 수정함수
  const postUpdateHandler = async e => {
    e.preventDefault();
    if (uploadImg.name !== undefined) {
      try {
        const imageRef = ref(storage, `test/${uploadImg.name}`);
        await uploadBytes(imageRef, uploadImg);    // 변경된 업로즈 이미지를 파이어베이스에 업데이트

        const downloadUrl = await getDownloadURL(imageRef);
        // 사진 수정 안되어도 값 안날라가게 고치기 필요
        const newPost = {
          title,
          content,
          image_url: downloadUrl,
          addrInput,
          buildingName,
          latitude,
          longitude,
        };

        const postRef = doc(db, 'fixs', id);
        await updateDoc(postRef, newPost);   //특정 포스트를 수정된 정 보로 업데이트
        toast.success('저장되었습니다.');
        navigate(`/detail/${id}`);
        return;
      } catch (err) {}
    }
    const newPost = {
      title,
      content,
      image_url: uploadImg,
      addrInput,
      buildingName,
      latitude,
      longitude,
    };

    const postRef = doc(db, 'fixs', id);           //아 이걸 두번하셨네 
    await updateDoc(postRef, newPost);    //파이어 베이스에 저장 되던 안되던인가? 흐음....
    toast.success('저장되었습니다.');
    navigate(`/detail/${id}`);
  };

  return (
    <div>
      <ScContainer>
        <ScMain onSubmit={postUpdateHandler}>
          <ScTitleBox>
            <ScTitleInput autoFocus value={title} onChange={titleOnchangeHandler} />
          </ScTitleBox>
          <ScLabel htmlFor="postImg" type="button">    //여기도 htmlFor로 인풋역할을 하게 하셔구먼
            <ScImgInput type="file" accept="image/*" id="postImg" onChange={imgOnclickHandler} />
            <ScImg src={previewImg} alt="" accept="image/*" />
          </ScLabel>
          <ScContentTextarea value={content} onChange={contentOnchangeHandler} />
          {/* <EditMap /> */}
          <div>
            <ScDivMapSearch>
              <div required onClick={searchAddress}>
                <input
                  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: '400px'}}>
                <MapMarker
                  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>
          </div>
          <ScBtnBox>
            <SubButton type="submit">수정완료</SubButton>
          </ScBtnBox>
        </ScMain>
      </ScContainer>
    </div>
  );
}
const ScContainer = styled.div`
  display: flex;
  justify-content: center;
  width: 100vw;
  height: 200vh;
`;
const ScMain = styled.form`
  width: 60%;
  height: 170vh;
  border: 2px solid #f6f6f6;
`;
const ScImg = styled.img`
  height: 30%;
  width: 100%;
  object-fit: cover;
`;
const ScLabel = styled.label`
  height: 350px;
  width: 100%;
`;
const ScImgInput = styled.input`
  display: none;
  height: max-content;
`;
const ScTitleInput = styled.input`
  margin-left: 30px;
  font-size: 30px;
  width: 100%;
  background: none;
  outline: none;
  border-width: 0 0 0px;
  padding: 25px;
`;
const ScTitleBox = styled.div`
  height: max-content;
  border-bottom: 1px lightgray solid;
  display: flex;
  align-items: center;
  background: none;
  outline: none;
`;

const ScContentTextarea = styled.textarea`
  display: flex;
  justify-content: center;
  width: 760px;
  height: 300px;
  margin: 50px 40px 0 40px;
  font-size: 15px;
  line-height: 35px;
  resize: none;
  background: none;
  border-width: 0 0 0px;
  outline: none;
`;
const ScBtnBox = styled.div`
  width: 93%;
  height: 70px;
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  margin: 30px 50px 0px 0px;
`;

const ScDivMapSearch = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  margin: 0 auto 0 auto;
  padding-left: 0;
  width: 70%;
  height: 24%;

  & 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;
    }
  }
`;

export default EditDetailPage;

//팀원들 코드들이 다 멋있군