React 프로젝트 (NoBroker)

이재영·2023년 8월 29일
0

React

목록 보기
11/12
post-thumbnail

7/28일 ~ 8/28일 React 프로젝트

프로젝트 제목 : NoBroker
프로젝트 주제 : 중개사 없는 부동산 매매 사이트

프로젝트 내용 :
기존에 중개사를 통한 부동산 매매 방식이 아닌,
판매자가 사이트에 매물을 등록하면 공인중개사 회원들의 투표에 따라
매물이 정상인지, 허위인지 정해지고 정상 매물은 사이트에 등록되어 거래가 가능해진다.
나중에 블록체인 수업을 듣고 지갑, 토큰 등 블록체인 기술을 적용 해봐야겠다.

프로젝트 페이지 구성 :
메인,로그인,회원가입,어드민,마이 페이지
매물 상세,투표,목록,등록페이지

내가 맡은 페이지 : 매물 등록, 마이페이지

페이지를 만들면서 새로 알게된 것들과 💡
오류 등 어려웠던 부분😡 을 정리해보자.

1. 💡 input 태그의 type="date"로 했을 때 날짜를 선택할 수 있는 구간을 막을 수 있다.📅

건축년도 이기떄문에 날짜를 미래로 설정하는건 맞지 않다고 생각하여 오늘 날짜까지만 선택이 가능하게끔 구현하고 싶었다. 위 사진과 같이 단순히

<input type="date">

하게된다면 모든 날짜 선택이 가능하지만,

const today= new Date(); // 현재 날짜 받아오기
  // yyyy-mm-dd 형태로 변환
  // 날짜 선택을 오늘까지로만 가능하게 막기
  var year = today.getFullYear();
  var month = ('0' + (today.getMonth() + 1)).slice(-2);
  var day = ('0' + today.getDate()).slice(-2);
  var dateString = year + '-' + month  + '-' + day;

<input type="date" max={dateString}>

현재 날짜를 받아와서 내가 자유롭게 yyyy-mm-dd 형식으로 만들고 max 속성을 정해주기만 하면 된다.


2.kakao 우편번호 서비스와 map api 이용하기 🗾


Post.jsx

// 주소찾기 버튼을 클릭하면 open이 실행되는데
// 팝업창에서 주소를 선택하여 onComplete 콜백함수가 실행되면
// handelComplete 함수가 실행된다.
const handleClick = () => {
    open({ onComplete: handleComplete});
  };
const scriptUrl = 'https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js';
// 주소검색하는 창을 팝업시켜준다.
const open = useDaumPostcodePopup(scriptUrl);

const handleComplete = (data) => {
  // data 안에는 api 안에서 제공하는 변수들의 값이 담긴다.
    console.log(data);
  }

data 안에는 어떤값이 있는지 아래 링크에서 확인할 수 있다.
https://postcode.map.daum.net/guide#sample

data 안의 address에 기본주소 값이 담겨있는데,
기본주소로 카카오 map api 를 이용하면 경도, 위도값을 찾아 맵과 마커를 표시할 수 있다.


MapApi.jsx
상위 컴포넌트인 Post.jsx 에서 data.address(기본주소) 값을 받아 placeAddress 에 담았다.

// encodeURL() 매개변수로 placeAddress 담아준다.
const url = `https://dapi.kakao.com/v2/local/search/address.json?query=${encodeURI(placeAddress)}`;

// url과 api 통해 경도, 위도 값을 가져와 state를 set 시켜준다. 
axios.get(url, {
headers: {
  //⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
      Authorization: `KakaoAK ${process.env.REACT_APP_REST_API_KEY}`,
    },
    }).then((e)=>{
        const data = e.data;
        setLng(data.documents[0].x); // 경도(lng)
        setLat(data.documents[0].y); // 위도(lat)
        setMapLoaded(true); //
    }).catch((err)=>{
        // console.log(err);
    });

// useEffect 를 통해 경도,위도,mapLoaded 값이 바뀌면 실행하도록 설정.
useEffect(()=>{

  if(mapLoaded){
        setIsNone("none"); // 지도 칸에 있는 p 태그 none

        const container = document.getElementById('map');
    
        const options = {
          // 지도 중심좌표
            center : new kakao.maps.LatLng(lat, lng),
          // 지도 확대레벨
            level :3
        };
    	// container 에 지도 생성
        const map = new kakao.maps.Map(container, options);
		// 해당 주소에 대한 좌표를 받아서
		var coords = new kakao.maps.LatLng(lat, lng);

        // 지도를 보여준다.
        container.style.display = "block";
        map.relayout();
 		
    	// 지도 중심을 변경한다.
        map.setCenter(coords);
        
		//마커를 미리 생성
        var marker = new kakao.maps.Marker({
            position: new kakao.maps.LatLng(lat, lng),
            map: map
        });
    
		// 마커를 결과값으로 받은 위치로 옮긴다.
        marker.setPosition(coords)

    }

    },[lat,lng,mapLoaded]);

  return (
    <>
    <div id='map' style={{width:'460px',height:'400px',display:'none'}}></div>
    </>
  )

어려웠던 점과 몰랐던 점😭

1. ${process.env.REACT_APP_REST_API_KEY}

위 코드의 ⭐ 가 되어있는 부분에${process.env.REACT_APP_REST_API_KEY} / .env 의 파일을 이용하여 API 키를 가져오는 코드가 있는데 단순히 REST_API_KEY 라고 하여 키를 저장하면 리액트에서 사용할 수 없고,
키 이름 앞에 REACT_APP를 붙여야 API 키를 사용할 수 있다.

2. React 라서 어디에 지도를 가져오는 api script를 어디에 작성해야할지 모름.

<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=your_api_key&libraries=services"></script>

/public/index.html 파일에 작성하여 해결.
참고사이트 : https://velog.io/@tpgus758/React%EC%97%90%EC%84%9C-Kakao-map-API-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

3. html 형식의 지도띄우는 코드를 React로 변환 후, new kakao.maps 의 kakao가 undefined 오류가 발생.

스크립트로 api를 가져오면 window 전역 객체에 들어가게 되는데,
함수형 컴포넌트에서는 바로 인식하지 못해 undefined 오류가 발생

const {kakao} = window;

window에서 kakao 객체를 뽑아서 사용. 위 사이트 참고함.

참고한 사이트

카카오 map 이용해 위도,경도 가져올 때 :https://well-made-codestory.tistory.com/36


3. FileReader()와 Multer mutiple 🖼️

imgMulter.jsx

	<MulterBox>
        <form encType="multipart/form-data">
          <FileDiv>
            {/* label 의 for 과 input의 id 는 서로 맞춰야한다. */}
            <FileLabel for="file">➕ 사진 추가</FileLabel>
            {/* name 의 upload 는 라우터의 upload.single(⭐"upload"⭐) 여기랑 맞춰야한다.
            ) */}
            <FileInput
              onInput={loadImg}
              type="file"
              name="upload"
              id="file"
              multiple
            />
          </FileDiv>

          <ImgBoxDiv id="imgCotainer"></ImgBoxDiv>
        </form>
      </MulterBox>
function loadImg(event) {
    let input = event.target;
    
    if(cnt<7 && input.files.length <8 && cnt + input.files.length < 8){

      setTemp(prevTemp => [...prevTemp, ...input.files]);
      const imgContainer = document.getElementById("imgCotainer");
      setCnt(preCnt => preCnt +parseInt(input.files.length));
      
    if (input.files) {
      let reader;
      for (let i = 0; i < input.files.length; i++) {
        let file = input.files[i];
        reader = new FileReader();

        reader.onload = function (e) {
          let img = document.createElement("img");
          img.src = e.target.result;

          let deleteButton = document.createElement('button');
          let div = document.createElement('div');
          deleteButton.textContent ="X";

          // x 버튼 눌렀을 때
          deleteButton.onclick=()=>{  
            imgContainer.removeChild(div);
            deleteButton.parentNode.removeChild(deleteButton);
            setCnt((prevCnt) => prevCnt - 1);
            setTemp(prevTemp => prevTemp.filter(item => item.name !== file.name));
          }

          div.appendChild(img);
          div.appendChild(deleteButton);
          imgContainer.appendChild(div);
         
        };
        // 이미지를 화면에 표시.
        reader.readAsDataURL(file);
      }
    }
    }
    else{
      alert("사진은 7개까지만 등록 가능합니다.");
    }
  }

insert.jsx

const form = new FormData();
    const files = temp;

    for (const key in estateInfoArr) {
      console.log(`key : ${key}, value : ${estateInfoArr[key]}`);
      form.append(`${key}`, `${estateInfoArr[key]}`);
    }
    for (let i = 0; i < files.length; i++) {
      // 라우터의 upload.single(⭐"upload"⭐) 여기랑 맞춰야한다.
      form.append("upload", files[i]);
    }

    console.log("------------------------------- files", files);
    setTemp([]);
    axios
      .post("/upload", form, {
        "Content-Type": "multipart/form-data",
        withCredentials: true,
      })

어려웠던 점과 몰랐던 점😭

1. label 태그의 for과 input 태그의 id의 이름을 맞춰야하고,
input 태그에 multiple 추가, 라우터의 single 아닌 array로 변경.

- input 태그의 name = "upload",
- 라우터의 Upload(컨트롤러이름).array("upload") 의 upload,
- form.append("upload") 3가지 부분을 동일하게 맞춰야한다.

2. temp 라는 배열이 있을 때, setTemp(preValue =>[...preValue, ...input.files] 의 코드는,
배열의 기존 값에 input.files의 값을 추가한다.


4. 컴포넌트 안에 같은 태그 여러개 있을 때, style-component 로 다른 스타일 주기 / &:nth-of-type() 🪄

// > : 직계 자식한테만 스타일 적용
& > div {
        // 공통으로 주고 싶은 스타일 속성 입력.
  
  		// 첫번째 div 태그한테만 주고 싶은 스타일 속성 입력.
        &:nth-of-type(1){
			// 첫번째 div안에 속해있는 span 태그한테 주고 싶은 스타일 속성 입력.
            & span{
            }
        }
  		// 두번째 div 태그한테만 주고 싶은 스타일 속성 입력.
        &:nth-of-type(2){
        }

5.useQuery 와 useMutation ⭐

💡이번 프로젝트에서 신기했던 기능중에 하나💡

- useQuery : 데이터를 서버에서 가져오는 작업을 간편하게 처리하기 위한 도구로, 상태관리 및 자동 갱신 등 효율적인 개발 가능.

마이페이지에서 유저정보 가져올 때

// 함수형으로 데이터를 가져오는 함수를 작성.
const getUserInfo = async () => {
        const data = await axios.get('/mypage/getUserInfo', {
            withCredentials: true,
        })
        return data.data;
    }
// "userInfo"는 쿼리키, getUserInfo 함수 실행하고,
// 나중에 Mutation 으로 정보수정을 했을 때, 쿼리키를 이용해 갱신한다.
// 로딩된 data는 userdata 에 담고,
// 데이터 로딩중 상태를 isLoading은 userdataLoading에 담고,
// 데이터 로딩시 오류 error는 userdataError에 담는다.
const { data: userdata, isLoading: userdataLoading, error: 
       userdataError } = useQuery('userInfo', getUserInfo)
// 상태관리
if (userdataLoading) {
        return <div>로딩 중...</div>;
    }
if (userdataError) {
        return <div>오류: {userdataError.message}</div>;
    }
// 받아온 data 는 userdata 에 담았고,
// userdata.user_id <- 이런 형식으로 사용가능.

- useMutaion : 데이터를 업데이트 하기 위한 API 호출을 간단하게 처리할 수 있고, 데이터 변경 요청이 성공,실패 했을 때 상황에 맞게 코드 작성가능, 캐싱 및 옵티마이제이션으로 성능 향상

* 캐싱 : 이전 데이터를 임시로 저장
* 옵티마이제이션 : 프로그램 실행 속도를 빠르게하거나 자원 사용량을 줄여 시스템을 효율적으로 개선,

마이페이지에서 유저정보를 수정할 때

// 수정하기 버튼을 눌렀을 때 실행되는 함수
// mutate() 로 mutation 이 실행된다.
const updateHandler = async () => {
        mutation.mutate();
    }
// updatemutation 함수에 axios.post 코드를 작성.
const updatemutation = async () => {
        const form = new FormData();
        form.append('userid', updateId);
        form.append('userphone', updatephone);
        form.append('useraddress', updateaddress);
        form.append('upload', updateImg);
const data = await axios.post('/mypage/update', form, {
            headers: {
                "Content-Type": "multipart/form-data",
            },
            withCredentials: true,
        })
        setisActive(!isActive);
  		// return 으로 반환해줘야 mutation에서 반환값을 통해 로직처리 가능.
        return data.data;
    }
// useQueryClient() 는 캐시관리, 데이터 업데이트, 쿼리 생성 및 관리 등 다양한 기능을 하며
// 그 중 invalidateQueries()는 특정 쿼리를 무효화 시키고 다시 불러온다
// 아까 useQuery에서 지정했던 쿼리키를 사용하면 처음에 가져온 정보를 무효화 시키고 업데이트 된 정보로 다시 가져오는것이다.
// 새로고침없이 웹 사이트의 정보가 갱신된다.
const queryClient = useQueryClient();
// useMutation 을 사용하여 updatemutation 을 실행해 정보를 업데이트.
const mutation = useMutation(updatemutation, {
  		// 업데이트 성공 시
  		// 서버에서 보내주는 msg로 식별해 로직 처리
        onSuccess: (data) => {
            if (data == "유저정보수정성공") {
                queryClient.invalidateQueries('update');
            }
        }
    })

6. 다른 input 태그 onChange에 같은 함수 쓰기

<UpdateboxInput>
    <label for="userphone">PHONE</label>
    <input onChange={onChangeHandler} id="userphone" name="userphone" placeholder={updatedata.phone}></input>
</UpdateboxInput>
<UpdateboxInput>
    <label for="useraddress">ADDRESS</label>
    <input onChange={onChangeHandler} id="useraddress" name="useraddress" placeholder={updatedata.address}></input>
</UpdateboxInput>

다른 input 태그인데 onChange에 같은 함수를 사용하고 싶었다.
여태까지는 e.target.value로 input에 적힌 값을 가져왔다.
근데, 함수를 같이 사용하다보니 e.target.value 에 적힌 값이 어디 input에서 적었는지 식별을 할 수가 없었다.
방법을 찾아보니..
위 코드처럼 input에 name 속성으로 명칭해주고,
아래 코드 처럼 e.target.name 를 fieldName에 담으면, 임의의 input에 값을 입력했을 때 input의 name이 fieldName에 담긴다.
그럼 if문을 통해 value 값을 원하는 변수에 set하면 된다.
e.target.previousElementSibling은 이벤트가 발생한 html 요소에 직전 요소를 말한다. input 태그에 값을 입력했으니 이벤트가 발생했고, 그 직전html 요소인 label을 의미하게된다.
그래서 value 값이 있으면 orange, 없으면 black 으로 색이 변한다.

const onChangeHandler = (e) => {
        const fieldName = e.target.name;
        const fieldValue = e.target.value;
        const labelElement = e.target.previousElementSibling;
        if (fieldValue) {
            labelElement.style.color = "orange";

            if (fieldName == 'userid') {
                setupdateId(fieldValue)
            }
            else if (fieldName == 'userphone') {
                setupdatephone(fieldValue)
            }
            else if (fieldName == 'useraddress') {
                setupdateaddress(fieldValue)
            }
        }
        else if (fieldValue == "") {
            labelElement.style.color = "black";
        }
    }

7. tap 기능 구현

const [componentsValue, setComponentsValue] = useState("");

{componentsValue === 'Account' && <Account />}

해당 탭을 눌렀을 때 setcomponentsValue("Account")로 componentsValue 의 값을 set해주고, 위 코드를 통해 componentsValue === 'Account' 일때만 Account 컴포넌트가 렌더링 되게끔 한다.


8. pdfkit 으로 pdf 만들기


판매자가 판매승인 tap에서 승인했을 때, 구매자가 구매중 tap에서 계약서 다운로드 를 했을 때, PDF 파일이 브라우저 상에 노출되어 다운로드가 가능하게끔 구현하고자 했다.

pdf 를 만드는 주요 코드

// PDF문서를 doc에 생성
const doc = new PDFDocument()

// 파일이름을 지정
const fileName = 'contract.pdf';

// fs 모듈을 사용해 fileName에 파일 쓰기 스트림을 생성.
const stream = fs.createWriteStream(fileName);

// doc 와 stream 을 연결해 pdf 문서가 생성될 때 그 내용이 해당파일에 쓰여짐.
doc.pipe(stream);

// doc.rect(시작 x, 시작 y, 박스 width, 박스 height) : 박스를 그리는 코드
// fillColor(): 박스를 채울 색상 지정
// strokeColor(): 테두리르 그릴 색상 지정
// fillAndStroke(): 지정한 색으로 바탕 채우고 테두리를 그림
// stroke() : 지정한 색으로 테두리만 그림
doc.rect(30, 170, 90, 20).fillColor('#EEEEEE').strokeColor('#000000').fillAndStroke().stroke();

doc.font(fontPath).fontSize(12).fillColor('#000000').text("소재지", 40, 170, { align: 'left' });

// 이미지 그리기(경로, 시작 x ,시작 y , {이미지 width , 이미지 height}
doc.image(imgPath, 530, 500, { width: 50, height: 50 });
doc.end();

// 브라우저 상에서 pdf 파일을 다운로드하여 볼 수 있도록 클라이언트로 전송하는 코드
stream.on("finish", () => {
	res.setHeader('Content-Type', 'application/pdf');
	res.setHeader('Content-Disposition', 'attachment; filename=contract.pdf');
	const readStream = fs.createReadStream(fileName);
	readStream.pipe(res);
});

stream.on("error", () => {
  console.log("pdf error");
});

생성된 PDF 클라이언트에서 파일로 다운로드

onSuccess : async(data)=>{
	try {
		const blob = new Blob([data.data], { type: 'application/pdf' });
		const downloadLink = document.createElement('a');
		downloadLink.href = URL.createObjectURL(blob);
		downloadLink.download = 'contract.pdf';
		downloadLink.click();

9. 토스페이 API로 입금 처리


사이트의 잔고에 돈을 입금하는 방법을 토스 API를 이용했다.

토스페이 개발자센터 사이트 :
https://docs.tosspayments.com/guides/learn/virtual-account-webhook 에 자세하게 설명이 되어있다.

그외의 내가 수정한 부분은 bin 파일을 app.js에 옮겼고,
입금요청 후 개발자센터에서 입금처리를 눌러주면 훅이 전달되는데
요청이 완료 되었을 때 req.body.data.status 에 'DONE' 반환되어
if문을 통해 서버 정보를 업데이트하여 입금처리 로직 구현

// 웹훅 받을 엔드포인트 추가하기
router.post("/hook", async function (req,res){
  
  if(req.body.data.status=='DONE'){
    const user_id = req.body.data.orderName.split("님")[0];

  await User.update({
      won: sequelize.literal(`won + ${req.body.data.totalAmount}`),
    }, {
      where: { user_id: user_id }
    })
  }
  /* 돌아온 웹훅 페이로드를 처리하는 코드를 추가해주세요. */
  res.status(200).end() // 성공 응답 보내기
})

완성된 프로젝트 git 주소 : https://github.com/zam0ng/React_Project_NoBroker

프로젝트에서 내가 담당하지 않아서 따로 해봐야하는 부분과
이 프로젝트에 추가하고 싶은 부분.

  • 블록체인 수업을 배우고 토스 api 가 아닌 지갑을 연동하여 구현해봐야겠다.
  • 이번에는 배포하는 과정을 거의 참여하지 못해서, 따로 배포도 해봐야 될거 같다. 프론트와 백 나눠서 2개의 서버로 배포 해볼 예정 (프론트는 s3 , 백은 ec2)
profile
한걸음씩

0개의 댓글

관련 채용 정보