FRONTENDSCHOOL6 / ready-act

멋쟁이사자처럼 파이널프로젝트 16조
MIT License
0 stars 4 forks source link

[R09M App] CreateRoom 페이지 - Map API 분리 이슈 #15

Closed jellyjoji closed 9 months ago

jellyjoji commented 9 months ago

내용

kakao Map API 를 구현한 다음 component 로 독립적인 페이지인 Location.jsx 로 분리하는 작업을 하고있습니다. 만날 장소 등록하기를 클릭하면 새 페이지에서 열리는 방식으로 분리했을때 지도에서 새 페이지에서 지도를 눌러 얻은 결과값을 현재 작업 중인 createRoom page 로 불러와야합니다. 해당 결과값을 불러와야 하는 방식에 대해 논의드립니다.

참고 이미지 (선택)

아래와 같이 지도에 찍힌 주소 결과값을 아래와 같이 메인 createRoom 페이지의 form data 로 불러와서 입력하는 방식에 대해 질문 드립니다.

Screenshot 2023-09-14 at 3 58 24 PM Screenshot 2023-09-14 at 4 05 09 PM

(참고) 사용한 kakao map api 첨부 https://apis.map.kakao.com/web/sample/coord2addr/

※ 댓글에 이슈 해결 완료 후 결과 또는 해결 과정 이미지 첨부

jellyjoji commented 9 months ago

컴포넌트 언마운트와 상태 초기화

페이지가 변경되면 이전 페이지의 모든 컴포넌트는 언마운트(unmount) 됩니다. 즉, 모두 사라집니다. 덩달아 컴포넌트의 상태도 모두 사라지므로 다시 페이지로 접근했을 때 모든 상태는 초기화(Reset)됩니다.

현 상황에서는 해결 불가능 ⚠️

CreateRoom 에서 Location 페이지로 이동하는 순간 CreateRoom 페이지에서 사용자가 입력된 모든 정보는 사라집니다. 컴포넌트가 언마운트되었기 때문입니다. 반대로 Location 페이지에서 사용자가 선택한 주소 정보도 같은 이유로 사라집니다. 이유는 동일합니다.

앱 상태 관리의 필요성

페이지가 전환되어도 상태가 온전하게 유지되려면 컴포넌트 레벨에서 관리하는 것이 아니라 앱에서 상태를 관리하여야 합니다. 쉽게 말해 전역(global) 상태로서 관리해야 합니다.

앱 상태 관리 구현 방법 선택

React Context API 또는 Zustand 라이브러리를 사용해서 페이지가 전환되더라도 상태가 유지되도록 구현해야 합니다. 팀에서 선택한 방법으로 앱 상태 관리 코드를 작성하세요.

jellyjoji commented 9 months ago

코드 작성 가이드

Context API를 사용하는 방법을 이해할 수 있게 코드 예시를 작성해보겠습니다. image 답변드린 아래 파일에 작성된 주석을 참고해보세요.

App.jsx

import { createContext, useState } from 'react';

// 앱에서 사용될 공통 상태 관리를 위한 컨텍스트 객체를 생성합니다.
export const AppContext = createContext();

function App() {
    // "방 만들기" 폼 입력에 필요한 모든 상태를 관리합니다.
  const [createRoomForm, setCreateRoomForm] = useState({
        category:'',
        title: '',
        content: '',
        price: 0,
        pickup: null,
        payment: '',
        participateCounter: 0,
        meetingPoint: '',
        uploadImage: null,
        status: '',
        location: '',
    });

    // 공급할 앱 상태 값(value)를 작성합니다.
    const appState = {
        // 예: state
        createRoomForm,
        // 예: setState
        updateCreateRoomForm: (key, value) => {
            setCreateRoomForm((state) => {
                return {
                    ...state,
                    [key]: value,
                };
            });
        },
    };

  return (
    <AppContext.Provider value={appState}>
      <HelmetProvider>
        <div className="max-w-xl mx-auto mt-12 font-pretendard bg-purple-200">
                    {/* 모든 페이지에 앱 상태 공급 */}
          <RouterProvider router={router} />
        </div>
      </HelmetProvider>
      <Toaster />
    </AppContext.Provider>
  );
}

CreateRoom.jsx

import { AppContext } from '@/App';

function CreateRoom() {
    // 이전에 관리하던 모든 상태는 이제 직접 관리하지 않고 App에서 관리합니다.
    // CreateRoom 페이지 컴포넌트는 AppContext로부터 상태 값을 읽고 쓸 수 있습니다.
  const { createRoomForm, updateCreateRoomForm } = useContext(AppContext);

    // 방 만들기 폼 입력할 때마다 앱에 상태를 저장해야 합니다.
    // 그래야 Location 페이지로 이동할 때 기억이 되겠죠.
    // 그러므로 파일을 제외한 나머지는 사용자가 입력할 때 상태를 업데이트해야 합니다.
    // useRef를 사용하는 것이 아니라, updateCreateRoomForm 함수를 사용하세요.
    // ...

  return (...);
}

Location.jsx

import { AppContext } from '@/App';

function Location() {
    // 이전에 관리하던 모든 상태는 이제 직접 관리하지 않고 App에서 관리합니다.
    // Location 페이지 컴포넌트는 AppContext로부터 상태 값을 읽고 쓸 수 있습니다.
  const { updateCreateRoomForm } = useContext(AppContext);

    useEffect(() => {
        // Kakao Map API 코드
        // ...
    }, []); 

    const handleSetLocation = () => {
        // ...
        // Kakao Map API에서 사용자가 선택한 주소 정보 값을 상태에 업데이트
        updateCreateRoomForm('location', '주소 정보');
    };

    // 사용자가 지도에서 장소를 선택한 후 다시 CreateRoom 페이지로 이동하는 기능이 필요합니다.
    // ...

  return (...);
}
jellyjoji commented 9 months ago

해결된 코드 첨부

Location.jsx

import arrowLeft from '@/assets/icons/arrowLeft.svg';
import { useContext, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import './Location.module.css';
import { AppContext } from '@/App';
import Button from "../../components/Button";

const { kakao } = window;

function Location() {
  const { updateCreateRoomForm } = useContext(AppContext);

  const [data, setData] = useState();

  useEffect(() => {
    const mapContainer = document.getElementById('map'), // 지도를 표시할 div
      mapOption = {
        center: new kakao.maps.LatLng(37.57157200866145, 126.9763416696016), // 지도의 중심좌표
        level: 4,
      };

    const map = new kakao.maps.Map(mapContainer, mapOption);

    const geocoder = new kakao.maps.services.Geocoder();

    const marker = new kakao.maps.Marker(),
      infowindow = new kakao.maps.InfoWindow({ zindex: 1 });

    searchAddrFromCoords(map.getCenter(), displayCenterInfo);

    kakao.maps.event.addListener(map, 'click', function (mouseEvent) {
      searchDetailAddrFromCoords(mouseEvent.latLng, function (result, status) {
        if (status === kakao.maps.services.Status.OK) {
          let detailAddr = !!result[0].address.address_name
            ? result[0].address.address_name
            : '위치정보를 불러올수없음';
          setData(detailAddr);

          const content = '<div className="bAddr">' + detailAddr + '</div>';

          marker.setPosition(mouseEvent.latLng);
          marker.setMap(map);

          infowindow.setContent(content);
          infowindow.open(map, marker);
        }
      });
    });

    kakao.maps.event.addListener(map, 'idle', function () {
      searchAddrFromCoords(map.getCenter(), displayCenterInfo);
    });

    function searchAddrFromCoords(coords, callback) {
      geocoder.coord2RegionCode(coords.getLng(), coords.getLat(), callback);
    }

    function searchDetailAddrFromCoords(coords, callback) {
      geocoder.coord2Address(coords.getLng(), coords.getLat(), callback);
    }

    function displayCenterInfo(result, status) {
      if (status === kakao.maps.services.Status.OK) {
        const infoDiv = document.getElementById('centerAddr');

        for (let i = 0; i < result.length; i++) {
          if (result[i].region_type === 'H') {
            infoDiv.innerHTML = result[i].address_name;
            break;
          }
        }
      }
    }
  }, []);

  useEffect(() => {
    updateCreateRoomForm('meetingPoint', data);
  }, [data]);

  return (
    <div className="h-full">
      <div className="relative h-12">
        <p className="text-center py-3">지도에서 위치 확인</p>

        <Link to="/createroom">
          <img src={arrowLeft} alt="뒤로 가기" className="absolute top-3" />
        </Link>
      </div>

      <div className="map_wrap">
        <div id="map" className="w-full h-[420px]"></div>
        <div className="hAddr flex">
          <span id="centerAddr" className='p-4'>
            {data}
          </span>

          <Link to="/createroom" className="bg-white w-full absolute max-w-xl bottom-0 p-4 drop-shadow-2xl">
            <Button type="submit" className="activeButton lgFontButton w-full ">
              이 위치로 설정
            </Button>
          </Link>

        </div>
      </div>
    </div>
  );
}

export default Location;

MeetingPoint.jsx

import arrow from '@/assets/icons/arrow.svg';
import { Link } from 'react-router-dom';

function MeetingPoint({ title, ...restProps }) {
  return (
    <>
      <div className="flex justify-between w-full py-4 bg-white">
        <label>{title}</label>

        <Link to="/location">
          <img src={arrow} alt="만날 장소 지도 이동" {...restProps} />
        </Link>
      </div>
    </>
  );
}

export default MeetingPoint;

CreateRoom.jsx

import { AppContext } from '@/App';
import { pb } from '@/api/pocketbase';
import Button from '@/components/Button';
import FormInput from '@/components/FormInput';
import CreateHeader from '@/layout/CreateHeader';
import CategoryDropdown from '@/parts/create/CategoryDropdown';
import ContentTextarea from '@/parts/create/ContentTextarea';
import DatePicker from '@/parts/create/DatePicker';
import FileUpload from '@/parts/create/FileUpload';
import MeetingPoint from '@/parts/create/MeetingPoint';
import ParticipateCounter from '@/parts/create/ParticipateCounter';
import PaymentToggleButton from '@/parts/create/PaymentToggleButton';
import Status from '@/parts/create/Status';
import { ClientResponseError } from 'pocketbase';
import { useContext, useRef, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import Creator from '@/parts/create/Creator';

function CreateRoom() {
  const { createRoomForm, updateCreateRoomForm } = useContext(AppContext);

  const formRef = useRef(null);
  const categoryRef = useRef(null);
  const titleRef = useRef(null);
  const contentRef = useRef(null);
  const priceRef = useRef(null);
  const dateRef = useRef(null);
  const paymentRef = useRef(null);
  const ParticipateCounterRef = useRef(null);
  const uploadImageRef = useRef(null);
  const statusRef = useRef(null);

  const handleCreate = async (e) => {
    e.preventDefault();

    const categoryValue = categoryRef.current.value;
    const titleValue = titleRef.current.value;
    const contentValue = contentRef.current.value;
    const priceValue = priceRef.current.value;
    const dateValue = dateRef.current.value;

    const paymentValue = paymentRef.current.dataset.payment;
    const ParticipateCounterValue = Number(
      ParticipateCounterRef.current.textContent
    );

    const meetingPointValue = createRoomForm.meetingPoint;
    const creatorValue = createRoomForm.creator.id;

    const uploadImageValue = uploadImageRef.current.files[0];
    const statusValue = statusRef.current.value;

    const data = new FormData();

    data.append('category', categoryValue);
    data.append('title', titleValue);
    data.append('content', contentValue);
    data.append('price', priceValue);
    data.append('pickup', dateValue);
    data.append('payment', paymentValue);
    data.append('participateNumber', ParticipateCounterValue);
    data.append('meetingPoint', meetingPointValue);
    data.append('creator', creatorValue);
    data.append('participate', creatorValue);
    if (uploadImageValue) {
      data.append('uploadImage', uploadImageValue);
    }
    data.append("status", statusValue);

    for (const [key, value] of data.entries()) {
      console.log(key, value);
    }

    // return
    try {
      await pb.collection('products').create(data);

      // navigate('/products');

    } catch (error) {
      if (!(error instanceof ClientResponseError)) {
        console.error(error);
      }
    }
  }

  return (
    <>

      <Helmet>
        <title>방만들기</title>
      </Helmet>

      <div >
        <CreateHeader />

        <form
          encType="multipart/form-data"
          ref={formRef}
          onSubmit={handleCreate}
        >
          <div className="flex flex-col gap-4 p-4 relative"
          >

            <CategoryDropdown
              ref={categoryRef}
              title="카테고리"
              className="w-full defaultInput"
            />
            <FormInput
              ref={titleRef}
              type="text"
              placeholder="상품명을 입력해주세요."
              labelClassName="product name"
              inputClassName="defaultInput w-full"
              label="상품명"
            />
            <FormInput
              ref={priceRef}
              type="number"
              placeholder="0원"
              labelClassName="product price"
              inputClassName="defaultInput w-full"
              label="상품 가격"
            />
            <ContentTextarea
              ref={contentRef}
              title="내용"
              placeholder="공구 모임 주요내용을 알려주세요."
              className="w-full defaultInput"
              labelClassName="product content"
            />

            <DatePicker
              ref={dateRef}
              title="픽업 날짜"
              className="w-full defaultInput"
              labelClassName="date Picker"
            />

            <Status
              ref={statusRef}
              title="상태"
              className="w-full defaultInput "
              labelClassName="status"
            />

            <Creator />

            <PaymentToggleButton
              ref={paymentRef}
              title="정산 방법"
              labelClassName="payment"
            />

            <ParticipateCounter ref={ParticipateCounterRef} title="인원" />

            <MeetingPoint title="만날 장소" />

            <FileUpload
              ref={uploadImageRef}
              title="파일 업로드"
              className="bg-[#EBF8E8] p-4 rounded-lg text-primary-500"
            />
          </div>
          <div className="bg-white fixed bottom-0 max-w-xl w-full p-4 drop-shadow-2xl">
            <Button type="submit" className="activeButton lgFontButton w-full ">
              방 만들기
            </Button>
          </div>
        </form>
      </div>
    </>
  );
}

export default CreateRoom;