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;
//팀원들 코드들이 다 멋있군