Skip to content

Latest commit

 

History

History
309 lines (226 loc) · 28.6 KB

README.md

File metadata and controls

309 lines (226 loc) · 28.6 KB

green-maps

채식 지도 서비스 웹 앱

Table of Contents
  1. About The Project
  2. Built With
  3. Getting Started with Screenshots
  4. Trouble Shooting

About The Project

홈화면

Temporary Account

  • 임시 계정 👉 ID: test123 PW: goodtoseeyou^9^
    • 로그인이 필요한 서비스: 북마크, 좋아요, 게시글/댓글/식당 리뷰 작성
  • PWA 다운로드 가능

Summary

  • 데이터 구성: 서울시 지정·인증업소 현황(800개) + 웹 스크래핑

  • 검색 모드

    • 반경탐색 모드
      • 옵션: 300m, 500m (기본값), 1km, 2km, 3km
    • 지역탐색 모드
      • 옵션: 시도 / 시군구 (기본값: 현재 위치) * 위치정보 허용 필수
    • 키워드 검색
      • 업종, 채식 인증 여부, 정렬 선택
      • 정렬 기본값: 관련도
        • 샐러디 화곡을 검색했을 때 샐러디 (화곡역점)이 우선 검색
    • 공통: 업종필터
  • 식당 북마크(+ 좋아요)

    • 북마크 그룹으로 관리(기본값: 기본그룹), 추가 및 이동
    • 좋아요 목록 확인
  • 게시판(커뮤니티)

  • 나머지 스크린샷 참조


Built With

Getting Started with Screenshots

Map

  • 이 화면은 위치 액세스 권한이 허락되어야 사용할 수 있습니다.

  • 화면을 이동하면 현재 뷰의 좌표를 기준으로 해당 다각형 내에 있는 식당 정보를 가져와 마커를 그립니다.

  • 반경탐색 모드(기본값)와 지역탐색 모드 2가지가 있습니다. 반경 선택 시 반경 표시가 화면 내에 들어오도록 지도 레벨이 조절됩니다.

첫화면 반경탐색 반경탐색결과

  • 지역 탐색 시 현재 위치의 시/도가 기본값입니다. 주소 문자열을 나눠서 각각 시/도와 시군구로 분류한 후 선택한 지역의 식당 정보를 가져옵니다.
제자리로 지역탐색 지역탐색결과
제자리로 지역탐색 지역탐색결과

  • 검색 시 쿼리스트링으로 검색어를 요청 메시지에 보냅니다. 거리순 정렬 기능도 있으므로 body에 현재 위치도 담아 서버에 요청합니다.

  • 관련도 점수

    • 주소 7, 업종 3, 이름 2, 채식 인증 1
    • 주소 문자열은 법정동과 행정동이 같이 표기되어 있는 경우도 있기 때문에 검색어가 존재하는지만 검사합니다. e.g ...화곡로141(화곡동 698-2)의 관련도 ...화곡로141의 관련도와 같다.
업종필터 검색 검색결과
업종필터 검색 검색결과


Review

  • 식당 상세정보 페이지에서 리뷰를 작성할 수 있습니다.
첫화면 버튼클릭 리뷰작성
첫화면 버튼클릭 리뷰작성

  • 이미지 처리 과정

    • URL.createObjectURL(File): 사용자에게 받은 이미지를 Object.createURL로 만듭니다.

    • 랜덤파일명 생성: 이미지별 랜덤 파일 이름을 생성해줍니다.

    • 스토리지에 저장: AWS S3로부터 signedUrl을 할당받고 해당 주소로 PUT 요청을 보냅니다.

    • db에 저장: 스토리지에 저장이 완료되면 도큐먼트의 src 필드에 앞에서 생성한 랜덤 파일 이름을 저장합니다.

    • 불러오기: <img src={IMAGE_URL/client/random_file_name.extension} alt="desc"> 형식으로 사용

리뷰작성 페이지 결과 텍스트 리뷰
리뷰작성 페이지 결과 텍스트 리뷰

  • 수정 시 이미지가 있을 경우 완료 버튼 클릭 시 src에 amazon 문자열이 있는지 확인합니다.
    • amazon 문자열이 포함되어 있는 경우 이미지는 수정하지 않았으므로 텍스트만 처리
수정 모달 수정 페이지 수정 결과
수정 모달 수정 페이지 수정 결과


My List

  • 북마크와 좋아요 목록을 관리하는 화면입니다.
  • 회원가입 시 기본 그룹이 자동으로 생성됩니다.
  • 그룹별 아이콘과 그룹명을 선택하여 관리할 수 있습니다.
첫화면 새그룹 추가 새그룹 추가 결과
첫화면 새그룹 추가 새그룹 추가 결과

  • 식당을 북마크하면 기본적으로 기본 그룹에 속하게 됩니다.
  • 그룹을 이동하려면 식당이 속한 그룹 페이지로 진입한 후 복사 => 원본삭제 순서로 이루어집니다.
그룹 수정 그룹 내 목록 그룹 이동1
그룹 수정 그룹 내 목록 그룹 이동1
그룹 이동2 그룹 이동3 그룹 이동 결과
그룹 이동2 그룹 이동3 그룹 이동 결과


Community

  • 로그인 없이 게시판을 읽을 수 있습니다.
  • 오늘 게시된 글은 hh:mm 형식으로 나타납니다.
  • IntersectionObserver API로 페이지네이션된 데이터의 마지막 데이터를 감시하고 다음 페이지를 요청합니다.
첫화면 댓글입력 결과
첫화면 댓글입력 결과

  • 간단한 텍스트 편집이 가능합니다.
  • (작성완료 시) raw html 태그를 문자열로 저장
    {
        ...
        content: `<h1>안내 사항</h1>`
    }
  • (읽기) DOMpurify로 감싼 후 에디터 내부에 주입
    <TextEditor dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(htmlString) }} />
  • (편집 시) 꾸며진 텍스트를 다시 불러오기 위해 db에 저장되어 있는 raw html 문자열을 draftjs의 contentState로 변환
글쓰기 결과 모달
글쓰기 결과 모달
글 수정 말머리 검색
글 수정 말머리 검색


My Page

  • 프로필 섹션에서 계정 정보를 변경할 수 있습니다.
  • 프로필 사진을 삭제하거나 변경할 수 있습니다.
  • 닉네임을 변경할 수 있습니다.
    • 정규식 검사 => 중복 검사
  • 비밀번호를 변경할 수 있습니다.
    • 기존 비밀번호 일치 검사 => 정규식 검사
첫화면 프로필사진 변경 닉네임 변경
첫화면 로필사진 변경 닉네임 변경

  • 리뷰를 남긴 식당 이름과 리뷰를 모아볼 수 있습니다.
비밀번호 변경1 비밀번호 변경2 리뷰 모아보기
비밀번호 변경1 비밀번호 변경2 리뷰모아보기


Sign Up, Sign In

  • 토큰은 공개키를 사용하여 로그인 시 JWT로 발급합니다.
  • 회원가입 기능은 인풋별로 값을 검증하는 스테이지 구조의 UI입니다 e.g 아이디를 통과하지 못하면 닉네임 스테이지로 넘어갈 수 없습니다.
회원가입 메시지 로그인
회원가입 메시지 로그인

  • 로그인 유지 항목을 체크하면 쿠키 만료기한이 7일로 설정됩니다. 이외에는 세션 토큰이 설정됩니다.
  • 카카오 로그인 시 전달받은 사용자 iduserId로 등록하고 프로필 사진과 닉네임을 가져옵니다. 최초 로그인 시 userId를 등록하는 회원가입 로직이 실행됩니다.
화면전환 카카오 로그인 결과
화면전환 카카오 로그인 카카오 로그인 결과

Trouble Shooting

  • 서버 배포 후 db의 registeredAt 시간이 실제 시간과 차이나는 현상

    • 서버 빌드 환경변수에 TZ=Asia/Seoul 추가하여 해결
  • jwt 비대칭키 형식 문제

    • 올바른 환경변수 형식 사용

      // .env
      PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\nabcd...\n-----END RSA PRIVATE KEY---';
      
      // 환경변수 문자열 내부 개행문자를 실제 개행으로 변환
      const privateKey: string = process.env.PRIVATE_KEY.replace(/\\n/g, '\n');
  • 모바일 웹 100vh 기준 문제 해결

    • 데스크탑 크롬 웹에서 100vh는 주소창을 제외한 나머지 뷰의 높이지만, 모바일 웹에서는 주소창 포함 높이로 계산됩니다. 그래서 모바일에서는 주소창 높이만큼 밀려서 네비게이션 바가 보이지 않는 현상이 있었습니다.

    • 모바일 환경에서는 높이를 innerHeight로 계산하도록 수정

      function App({ children }: { children: React.ReactNode }) {
          const [isMobile, setIsMobile] = useState(false);
      
          const appRef = useRef<HTMLDivElement>(null);
      
          useEffect(() => {
              setIsMobile(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent));
          }, []);
      
          useEffect(() => {
              if (isMobile) {
                  const element = appRef.current as HTMLDivElement;
                  if (element !== null) {
                      element.style.height = window.innerHeight + 'px';
                  }
              }
          }, [isMobile]);
      
          return (
              <div className="app" ref={appRef}>
                  {children}
              </div>
          );
      }