Frond-end/React

[React] 네이버 지도 연동하기

Nellie Kim 2024. 3. 12. 17:21
728x90

 

 

index.tsx 

/* eslint-disable react-hooks/exhaustive-deps */
/***
 * 매장 상세 (CEO) 페이지
 */

import React, { useState, useEffect, useRef } from "react";
import { useRouter } from "next/router";
import Layout from "example/containers/Layout";
import ImageUploader, { ImageFile } from "../../../components/ImageUploader";
import { ImgModel, CodeModel } from "@apis/common";
import { getFormattedDate } from "utils/util";
import Dialog, {
  DialogProps,
  initDialog,
  showDlg,
} from "../../../components/Dialog";
import {
  getAllOption,
  getStore,
  updateStore,
  getImages,
  insertImages,
  delImage,
  InfoModel,
} from "@apis/store";
import PhoneNumberInput from "../../../components/PhoneNumberInput";


function StoreDetail() {
  const [editable, setEditable] = useState(true);
  const [allOptions, setAllOptions] = useState<CodeModel[]>([]);
  const [data, setData] = useState<InfoModel>({} as InfoModel);
  const mapElement = useRef(null);
  const [initImages, setInitImages] = useState<ImageFile[]>();
  let imageFiles: ImageFile[] = [];
  const [deletedImages, setDelImages] = useState<ImageFile[]>([]);
  const [dlgProps, setDlgProps] = useState<DialogProps>({} as DialogProps);
  initDialog(setDlgProps);

  // on page change, load new sliced data
  // here you would make another server request for new data
  useEffect(() => {
    fetchData();
  }, []);
  useEffect(() => {
    // 상점 데이터 대입이 완료 되면 지도초기화 & 상점 위치 설정
    initMap();
  }, [data]);

  async function fetchData() {
    // 상점에 세팅 가능한 전체 옵션 목록 조회
    let response = await getAllOption();
    if (response.status == 200) setAllOptions(response.data);
    // 상점 정보 조회
    response = await getStore();
    if (response.status != 200) return;
    setData(response.data); // 상점 정보 세팅
    response = await getImages();
    if (response.status == 200) {
      // 상점 첨부 이미지 조회
      const fileList = response.data.dataList as ImgModel[];
      const initList = fileList.map(
        ({ apndFileId, urlPath }) =>
        ({
          fileId: apndFileId,
          src: urlPath?.startsWith("/") ? urlPath : "/" + urlPath,
        } as ImageFile)
      );
      setInitImages(initList); // 초기 이미지 세팅
    }
  }

  function initMap() {
    // 지도 영역 생성
    if (!window.naver || !data) return;

    let lat = 37.29618649320232;
    let lng = 127.01751553242397;
    if (data.lat && data.lng) {
      lat = data.lat;
      lng = data.lng;
    }
    const mapOptions = {
      center: new window.naver.maps.LatLng(lat, lng),
      tileSpare: 10,
      zoom: 15,
    };

    const map = new window.naver.maps.Map(
      mapElement.current as any,
      mapOptions
    );

    // 목표 위치에 마크 표시하기
    const marker = new window.naver.maps.Marker({
      position: new window.naver.maps.LatLng(lat, lng),
      map: map,
    });

    // 마크 클릭 하면 표시 될 상세 창 만들기
    const infoWindow = new window.naver.maps.InfoWindow({
      content: "<div><b>" + data.name + "</b><br>" + data.addr1 + "</div>",
    });
    marker.addListener("click", function () {
      const shouldOpenInfoWindow = !infoWindow.getMap();
      if (shouldOpenInfoWindow) {
        infoWindow.open(map, marker);
      } else {
        infoWindow.close();
      }
    });

    // 자동으로 마크 클릭 효과(상세 창 표시)
    window.naver.maps.Event.trigger(marker, "click");
  }

  function handleCheckboxChange(opt: CodeModel, isChecked: boolean) {
    if (isChecked) {
      // 체크됐을 때: optionList에 opt 추가
      const updatedOptionList = data.storeOptionList
        ? [...data.storeOptionList, opt]
        : [opt];
      setData({ ...data, storeOptionList: updatedOptionList });
    } else {
      // 체크 해제됐을 때: optionList에서 opt 제거
      const updatedOptionList = data.storeOptionList
        ? data.storeOptionList.filter((item) => item.cdC !== opt.cdC)
        : [];
      setData({ ...data, storeOptionList: updatedOptionList });
    }
  }

  // ImageUploader의 조작에 대한 핸들러 함수들
  function onSelectImages(files: ImageFile[]) {
    imageFiles = files;
  }
  function onRemoveImage(file: ImageFile) {
    // 이미 디비에 등록 된 상점 사진의 경우는 디비에서의 삭제 처리까지 진행
    if (file.fileId) setDelImages((prev) => [...prev, file]);
  }

  async function onUpdate() {
    if (
      !data.name ||
      !data.tel ||
      !data.intro ||
      !data.openTime ||
      !data.payInfo ||
      !data.parkInfo
    ) {
      showDlg("필수 값이 입력되지 않았습니다.<br><br> (편의시설 외 모든 항목이 필수)<br><br> 다시 확인해 주세요.");
      return;
    }

    const formData = new FormData();
    imageFiles.forEach((img) => img.org && formData.append("files", img.org));

    console.log("update data = ", data);

    let result: boolean = (await updateStore(data)).status === 200;
    if (result) {
      deletedImages.forEach((file) =>
        delImage({ apndFileId: file.fileId } as ImgModel)
      );
    }
    if (result) result &&= (await insertImages(formData)).status === 200;

    if (result) {
      showDlg("상점 데이터 저장을 완료했습니다.");
      fetchData(); // 데이터 다시 로드
    } else {
      showDlg("상점 데이터 저장에 실패하였습니다.");
    }
  }

  return (
    <Layout>
      <div className="content">
        <div className="page-title">매장관리</div>
        <div className="box">
          <table className="write-table">
            <colgroup>
              <col style={{ width: "200px" }} />
              <col />
            </colgroup>
            <tbody>
              <tr>
                <th>매장명</th>
                <td>
                  <div className="input-group">
                    <input
                      type="text"
                      value={data.name || ""}
                      readOnly={!editable}
                      onChange={(e) =>
                        setData({ ...data, name: e.target.value })
                      }
                    />
                    {data.idx && !data.name && (
                      <div style={{ color: "red" }}>필수 항목</div>
                    )}
                  </div>
                </td>
              </tr>
              <tr>
                <th>매장 연락처</th>
                <td>
                  <div className="input-group">

                    <PhoneNumberInput value={data.tel ? data.tel.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3') : ""} onValueChange={(value) => setData({ ...data, tel: value })} readOnly={!editable} />

                    {data.idx && !data.tel && (
                      <div style={{ color: "red" }}>필수 항목</div>
                    )}
                  </div>
                </td>
              </tr>
              <tr>
                <th>매장주소</th>
                <td>
                  <div className="input-btn-group flex-start">
                    <input type="text" disabled value={data.addr1 || ""} />
                    <button className="btn btn-border">검색</button>
                  </div>
                  <div
                    ref={mapElement}
                    className="mt-2"
                    style={{ border: "1px solid black", height: "300px" }}
                  ></div>
                </td>
              </tr>
              <tr>
                <th>매장 소개</th>
                <td>
                  <div className="input-group">
                    <textarea
                      value={data.intro || ""}
                      readOnly={!editable}
                      onChange={(e) =>
                        setData({ ...data, intro: e.target.value })
                      }
                    ></textarea>
                    {data.idx && !data.intro && (
                      <div style={{ color: "red" }}>필수 항목</div>
                    )}
                  </div>
                </td>
              </tr>
              <tr>
                <th>운영 시간</th>
                <td>
                  <div className="input-group">
                    <textarea
                      value={data.openTime || ""}
                      readOnly={!editable}
                      onChange={(e) =>
                        setData({ ...data, openTime: e.target.value })
                      }
                    ></textarea>
                    {data.idx && !data.openTime && (
                      <div style={{ color: "red" }}>필수 항목</div>
                    )}
                  </div>
                </td>
              </tr>
              <tr>
                <th>이용 요금</th>
                <td>
                  <div className="input-group">
                    <textarea
                      value={data.payInfo || ""}
                      readOnly={!editable}
                      onChange={(e) =>
                        setData({ ...data, payInfo: e.target.value })
                      }
                    ></textarea>
                    {data.idx && !data.payInfo && (
                      <div style={{ color: "red" }}>필수 항목</div>
                    )}
                  </div>
                </td>
              </tr>
              <tr>
                <th>
                  편의시설
                  <br />및 서비스
                </th>
                <td>
                  <div className="radio-check-list row">
                    <ul>
                      {allOptions.map((opt, idx) => (
                        <li key={`options${idx + 1}`}>
                          <div className="checkbox">
                            <input
                              type="checkbox"
                              name="options"
                              id={`options${idx + 1}`}
                              checked={
                                data.storeOptionList
                                  ? data.storeOptionList.some(
                                    (item) => item.cdC === opt.cdC
                                  )
                                  : false
                              }
                              disabled={!editable}
                              onChange={(e) => {
                                handleCheckboxChange(opt, e.target.checked);
                              }}
                            />
                            <label htmlFor={`options${idx + 1}`}>
                              {opt.cdNm}
                            </label>
                          </div>
                        </li>
                      ))}
                    </ul>
                  </div>
                </td>
              </tr>
              <tr>
                <th>주차 안내</th>
                <td>
                  <div className="input-group">
                    <textarea
                      value={data.parkInfo || ""}
                      readOnly={!editable}
                      onChange={(e) =>
                        setData({ ...data, parkInfo: e.target.value })
                      }
                    ></textarea>
                    {data.idx && !data.parkInfo && (
                      <div style={{ color: "red" }}>필수 항목</div>
                    )}
                  </div>
                </td>
              </tr>
              <tr>
                <th>
                  매장 사진
                  <br />
                  (최대 10장)
                </th>
                <td>
                  <ImageUploader
                    initImages={initImages}
                    onSelected={onSelectImages}
                    onRemove={onRemoveImage}
                  />
                </td>
              </tr>
              <tr>
                <th>마지막 저장일시</th>
                <td>
                  <div className="input-group">
                    <label>{getFormattedDate(data.updateDt, true, true)}</label>
                  </div>
                </td>
              </tr>
            </tbody>
          </table>
          <ul className="submit-btn">
            <li>
              <button className="btn btn-green" onClick={onUpdate}>
                저장하기
              </button>
            </li>
          </ul>
        </div>
      </div>
      <Dialog props={dlgProps} />
    </Layout>
  );
}

export default StoreDetail;