Skip to content

Latest commit

 

History

History
667 lines (500 loc) · 39.5 KB

README.md

File metadata and controls

667 lines (500 loc) · 39.5 KB

1. 프로젝트 소개

holo_main_logo

'홀로냠냠'은 혼밥을 즐기는 사람들을 위한 커뮤니티 플랫폼입니다.

이전에는 가족 단위의 식사가 주를 이루었지만, 최근 1인 가구의 증가, 바쁜 생활 패턴, 개인의 취향을 중요시하는 트렌드 등 여러 요인에 의해 요즘은 혼자서 식사를 하는 '혼밥' 문화가 확산되고 있습니다. 그러나 혼밥이 편리하긴 하지만, 때로는 외로움을 느낄 수 있으며, 맛있는 식당을 찾는 것이 어려울 수도 있습니다.

이러한 문제를 해결하기 위해 저희는 '홀로냠냠' 프로젝트를 기획하게 되었습니다. '홀로냠냠'은 혼밥을 즐기는 사람들을 위한 커뮤니티로, 사용자들은 자신만의 혼밥 식당을 추천하고, 다른 사용자의 게시글을 통해 새로운 식당을 발견할 수 있습니다.

이 프로젝트의 목표는 혼밥 문화를 즐기는 사람들이 외로움을 덜고, 서로 정보와 경험을 나눌 수 있도록 하는데 중점을 두고 있습니다. 이를 통해 혼밥 문화를 즐기는 사람들이 더 다양하고 풍부한 식사 경험을 할 수 있도록 돕고자 합니다.

그럼, '홀로냠냠'과 함께 즐거운 혼밥하세요!

🍰 배포 사이트 : https://holonyam.netlify.app
🍔 테스트 계정[email protected]
🍟 비밀번호: holo_nyam

2. 목차

  1. 프로젝트 소개
  2. 목차
  3. 팀 소개
  4. 역할 분담
  5. 개발 스택
  6. 개발 환경
  7. 구현 기능
  8. 기능 UI
  9. 폴더 구조
  10. 작업 문화
  11. 후기

3. 팀 소개

안녕하세요. 끈질기게 도전하는 4명의 프론트엔드 개발자로 이루어진 “산전수전공중전”팀입니다.

정승규 오수민 김모건 정현지

GitHub

GitHub

GitHub

GitHub
팀장 팀원 팀원 팀원
피그마 노션
팀 Figma 팀 Notion

4. 역할 분담

Frame 8

5. 개발 스택

사용기술
패키지
포멧터
협업
디자인
IDE
배포

6. 개발 환경

채택한 기술

Node.js

  • JavaScript 실행 환경으로 채택한 Node.js는 프론트엔드와 백엔드에서 동일한 언어(JavaScript)를 사용할 수 있며, 개발 생산성을 향상시키는 다양한 도구와 라이브러리가 존재합니다.

React.js

  • JavaScript 라이브러리의 하나인 React는 컴포넌트 단위로 파일을 분리하여 구성할 수 있기 때문에 효율적인 소스 코드 개발이 가능하여 협업 프로젝트에 적합합니다.
  • 그리고 동적 웹 어플리케이션 개발에 있어서, Virtual DOM을 사용하여 필요한 부분만 다시 렌더링하는 방식의 DOM 업데이트 최적화를 통해 성능적인 이점을 가집니다.

npm

  • 대표적인 Node.js 환경의 패키지 관리자에는 npm과 yarn이 있습니다.
  • 두 가지 프로그램 모두 npm registry에 게시된 패캐지를 사용할 수 있으며 종속성을 관리합니다.
  • yarn은 패키지 설치와 보안 측면에서 npm 보다 우수한 성능을 가지지만, 우리의 프로젝트에서는 보안상 문제없는 최소한의 패키지 모듈을 사용하므로 추가 설치가 필요 없는 npm을 사용했습니다.

Styled-component

  • Pure CSS는 전역 관리로 인해 팀원의 코드에 영향을 미칠 수 있으며, 유지보수 측면에서 모듈화가 어려워 프로젝트가 크고 고도화 될수록 시간과 비용이 비례하여 증가한다는 단점이 있습니다.
  • Styled-compoment는 CSS-in-JS로 JavaScript의 변수와 함수 그리고 React의 Props를 활용한 조건부 스타일링이 가능하다. CSS를 컴포넌트 단위로 모듈화 가능하며, 짧은 길이의 유니크한 클래스를 자동으로 생성하기 때문에 코드 경량화 및 유지보수에 용이합니다.

Axios

  • Node.js와 브라우저에서 Promise 기반으로 비동기 작업을 처리하는 데 있어서 XMLHttpRequest와 fetch 보다 직관적이고 간결한 코드 작성이 가능합니다.

Recoil

  • React를 위한 상태 관리 라이브러리로 Recoil을 사용한 이유는 전역에서 필요한 상태를 관리하기 위해서 입니다. 기존 Redux나 redux-toolkit 보다 초기 세팅 및 사용법이 간단하여 코드가 간결해집니다.

감귤마켓 API

  • 온전히 프론엔드 구현에 집중할 수 있도록 프론트엔드 스쿨에서 제공하는 서버 API 입니다.
  • 로그인, 회원가입, 프로필, 게시글, 댓글 등의 커뮤니티와 관련 기능을 제공합니다.

Kakao API

  • Kakao 지도 API와 카카오톡 공유하기 API를 활용하였습니다.
  • Kakao API는 다양한 JavaScript SDK 예시와 국문의 공식 문서로 개발 접근성이 좋습니다. 또한 일 30만 건의 API 호출 내에서 모든 기능을 무료로 이용할 수 있어, 비용 측면에서도 이점을 가지고 있습니다.

Netlify

  • 웹 호스팅 서비스를 제공하는 플랫폼은 많지만, Netlify는 GitHub 연동, 간단한 사용법, 500개의 사이트를 일정 성능까지 무료 사용이 가능하여 채택했습니다.
협업

Notion - 회의록, 동시 문서 작업 및 문서 관리에 활용했습니다.
Discord - 음성 채팅방을 활용해 스크럼, 정기 회의 등의 의사소통 도구로 사용했습니다.
Figma - 프로젝트 기획과 UI 디자인, 와이어프레임 개발을 수행했습니다.
Git, GitHub

  • 소스 코드 버전 관리에 Git을 활용 했고, Git 호스팅 사이트로는 GitHub를 사용하여 프로젝트 저장, 내장된 칸반 보드 및 간트 차트를 이용해 프로젝트 이슈 및 일정 관리를 하였습니다.

Visual studio Code

  • 확장 프로그램인 Live Share를 통해 Pair programing를 수행했습니다.
  • Git과 연동하여 소스 코드 버전 관리에 Git graph 활용했습니다.

7. 구현 기능

 🍕 계정

- splash 페이지
- 로그인,로그아웃 페이지/유효성 검사
- 회원가입
- 프로필 페이지/유효성 검사(+이미지)
- welcome 페이지

 🍔 피드

- 게시글 등록 /수정/삭제
- 모달창
- 이미지 최대 3장 업로드
- 유저 게시글 목록
- 팔로잉 게시글 목록
- 게시글 신고
- 게시물 좋아요 및 취소

 🥨 댓글

- 댓글 등록/수정/삭제/날짜표시
- 신고하기 UI 구현

 🍤 프로필

- 개인 / 타인 프로필 페이지
- 프로필 수정
- 팔로우/언팔로우 UI 버튼 기능
- 팔로우/팔로잉 리스트

 🌮 채팅

- 채팅 목록 (UI)
- 채팅 페이지/이미지 업로드 (UI)

 🥤 맛집평가

- 맛집 등록/별점
- 맛집 수정/삭제
- 맛집 모달창
- 맛집 상세
- 지도 API (홀로냠냠의 특수기능)
Kakao API
  • Kakao API

    1. 카카오 지도 API

      • geocoder.addressSearch를 이용하여 주소를 좌표로 변환하고, 그 좌표를 이용하여 마커와 정보창을 생성합니다.

      • 마커의 이미지는 MarkerImgSvg에서 가져오며, 크기는 kakao.maps.Size를 이용하여 설정합니다.

        geocoder.addressSearch(placeLink, function (result, status) {
              if (status === kakao.maps.services.Status.OK) {
                let coords = new kakao.maps.LatLng(result[0].y, result[0].x);
        
                let imageSrc = MarkerImgSvg,
                  imageSize = new kakao.maps.Size(64, 69);
        
                let basicMarkerImage = new kakao.maps.MarkerImage(imageSrc, imageSize),
                  markerPosition = new kakao.maps.LatLng(result[0].y, result[0].x); //
        
                let basicMarker = new kakao.maps.Marker({
                  map: map,
                  position: markerPosition,
                  image: basicMarkerImage,
                });
        
                let content = `<div class="customoverlay"><a href="https://map.kakao.com/link/to/${placeName},${coords.Ma},${coords.La}" title="길찾기 버튼"><span class="name">${placeName}</span></a></div>`;
        
                customOverlay = new kakao.maps.CustomOverlay({
                  position: markerPosition,
                  content: content,
                  yAnchor: 1,
                });
        
                map.setCenter(coords);
                customOverlay.setMap(map);
                basicMarker.setMap(map);
              }
            });
      • kakao.maps.event.addListener를 통해 지도(map)에 클릭 이벤트를 연결합니다. 사용자가 지도를 클릭하면 실행되는 함수에는 클릭 이벤트에 대한 정보가 mouseEvent로 전달됩니다

      • mouseEvent.latLng를 통해 클릭한 위치의 좌표를 얻고, marker.setPosition(position)을 통해 마커의 위치를 클릭한 위치로 이동시킵니다.

      • toggleRoadview(position)을 호출하여 해당 좌표의 로드뷰를 활성화합니다. 이때 toggleRoadview 함수 내부에서는 rvClient.getNearestPanoId를 이용하여 가장 가까운 파노라마 ID를 가져와 로드뷰를 설정합니다.

        kakao.maps.event.addListener(map, 'click', function (mouseEvent) {
          if (!overlayOn) {
            return;
          }
        
          let position = mouseEvent.latLng;
          marker.setPosition(position);
          toggleRoadview(position);
        });
    2. 카카오 공유 API

      • kakao.Share.sendDefault 메소드를 이용하여 공유할 정보를 설정하고 공유합니다.

      • 공유할 정보는 objectType, content, buttons 등 다양한 속성을 가질 수 있습니다.

      • 각각의 정보를 설정하고 공유 버튼을 클릭하면 해당 정보가 카카오 공유를 통해 전송됩니다.

        kakao.Share.sendDefault({
                objectType: 'location',
                address: placeInfo.link,
                addressTitle: placeInfo.itemName,
                content: {
                  title: placeInfo.itemName,
                  imageUrl: placeInfo.itemImage,
                  description: placeInfo.link,
                  link: {
                    mobileWebUrl: 'https://holonyam.netlify.app/',
                    webUrl: 'https://holonyam.netlify.app/',
                  },
                },
                social: {
                  likeCount: placeInfo.price,
                },
                buttons: [
                  {
                    title: '웹으로 보기',
                    link: {
                      mobileWebUrl: 'https://holonyam.netlify.app/',
                      webUrl: 'https://holonyam.netlify.app/',
                    },
                  },
                ],
              });
최신순/별점순 조회 기능
  • 최신순/별점순 조회 기능
    • 이 기능은 Recoil 라이브러리의 atom을 사용하여 구현되었으며, viewBtnState라는 atom을 생성하여 초기 정렬 상태를 '별점순'으로 설정하였습니다.

      export const viewBtnState = atom({
        key: 'viewBtnState',
        default: '별점순',
        effects_UNSTABLE: [persistAtom],
      });
    • 사용자의 버튼 클릭에 따라 viewMode라는 상태 값을 변경하여 게시글의 정렬 순서를 변경하게 됩니다. 이 viewModeviewBtnState atom과 연결되어 있어, 해당 상태를 관리하게 됩니다.

      const [viewMode, setViewMode] = useRecoilState(viewBtnState);
      
      const handleViewModeChange = (mode) => {
        if (viewMode === '최신순') {
          mode = '별점순';
        }
        setViewMode(mode);
      };
      
      <ButtonWrap>
        <SortButton onClick={() => handleViewModeChange('최신순')}>
          <Star src={StarImg} />
          &nbsp;{viewMode}으로 보기&nbsp;
        </SortButton>
      </ButtonWrap>
    • sort() ****메서드를 이용하여 만약 viewMode가 '별점순'이라면 b.updatedAt - a.updatedAt을 반환하고, '최신순'이라면 b.price - a.price를 반환하도록 하였습니다.

      placeInfo
        .sort((a, b) => {
          if (viewMode === '별점순') {
            return b.updatedAt - a.updatedAt;
          } else if (viewMode === '최신순') {
            return b.price - a.price;
          }
        })
Carousel 기능

Untitled

Carousel의 핵심은 Left/RightButton와 CarouselImages 컴포넌트로 구성됩니다. 

CarouselImages 컴포넌트는 carouselImages 배열의 각 요소를 매핑하여 이미지를 렌더링하고 currentIndex 상태값에 따라 활성화 또는 비활성화 됩니다. 

Left/RightButton 컴포넌트는 각각 이전과 다음 이미지를 볼 수 있도록 handlePrevious 또는 handleNext 함수를 호출하여 currentIndex 상태값을 변경하는 역할을 합니다. 추가적으로 각 img 태그에는 렌더링 성능을 고려하여 loading=’lazy’를 추가하여, 뷰포트 내에 위치하게 되었을 때 load를 하도록 했습니다. (lazy Loading 기능)

...
const [currentIndex, setCurrentIndex] = useState(0);
  const handlePrevious = () => {
    setCurrentIndex((prevIndex) =>
      prevIndex - 1 < 0 ? carouselImages.length - 1 : prevIndex - 1,
    );
  };
  const handleNext = () => {
    setCurrentIndex((prevIndex) =>
      prevIndex + 1 === carouselImages.length ? 0 : prevIndex + 1,
    );
  };
  return (
    <CarouselWrapper>
      <CarouselImages>
        {carouselImages?.map((imgItem, index) => (
          <img
            key={index}
            src={
              previews
                ? imgItem
                : imgItem.trim().startsWith('https://')
                ? imgItem
                : `https://api.mandarin.weniv.co.kr/${imgItem.trim()}`
            }
            className={currentIndex === index ? 'active' : 'inactive'}
            alt={previews ? userInfo : `포스트이미지 by @${images.userInfo}.`}
            crossOrigin='anonymous'
            loading='lazy'
            onClick={onImageClick}
            style={{ cursor: detail === true ? 'default' : 'pointer' }}
          />
        ))}
      </CarouselImages>
      {carouselImages.length > 1 && (
        <div>
          <LeftButton onClick={handlePrevious}>
            <img src={Left} alt='이전 사진 보기 화살표 버튼' />
          </LeftButton>
          <RightButton onClick={handleNext}>
            <img src={Right} alt='다음 사진 보기 화살표 버튼' />
          </RightButton>
        </div>
      )}
...
Drag&Drop 기능

드래그앤드랍

드래그가 시작되면 dragStart() 함수를 호출되어 사용자가 선택한 미리보기 이미지의 DOM 요소 인덱스를 **useRef()**를 사용해 dragItem.current에 저장합니다.

드래그 중인 대상이 위로 포개졌을 때는 dragEnter() 함수가 호출되어 dragOverItem.current에 해당 인덱스를 저장합니다.

사용자가 커서를 뗐을 때, drop() 함수가 호출되어 드래그 전 uploadPreview ****배열을 복사한 후, dragItem.currentdragOverItem.current를 이용하여 순서를 변경하고 newPreviewList 에 저장하고 **setUploadPreview()**을 통해 새로운 이미지 순서로 반영합니다.

const dragItem = useRef(); // 드래그할 아이템의 인덱스
const dragOverItem = useRef(); // 포개진 아이템의 인덱스
...
const dragStart = (e, position) => { dragItem.current = position; };
// 드래그중인 대상이 위로 포개졌을 때
const dragEnter = (e, position) => { dragOverItem.current = position; };
// 커서 뗐을 때
const drop = () => {
    const newPreviewList = [...uploadPreview];
    const dragItemValue = newPreviewList[dragItem.current];
    newPreviewList.splice(dragItem.current, 1); // delete
    newPreviewList.splice(dragOverItem.current, 0, dragItemValue); // insert
		setUploadPreview(newPreviewList);
    dragItem.current = null;
    dragOverItem.current = null;
}
...
{uploadPreview?.map((preview, index) => (
    <UploadImgDiv key={index}>
    <CloseImgBtn
        onClick={(event) => {
        event.preventDefault(); // 기본 동작 취소
        removeImg(index);
        }}
    />
    <UploadImg
       draggable
       onDragStart={(e) => dragStart(e, index)}
       onDragEnter={(e) => dragEnter(e, index)}
       onDragEnd={drop}
       onDragOver={(e) => e.preventDefault()}
       key={index}
       src={preview}
       alt='업로드된 이미지'
    >
    </UploadImgDiv>
))}
...

8. 기능 UI

스플래쉬 로그인 회원가입
welcome_splash login signup
회원가입 프로필 설정 계정 검색 팔로워&팔로잉
profilesetting search following follower
메인 좋아요 댓글 등록&수정
main like comment_create_delete
게시물 상세 채팅 냠냠피드 작성 (게시물 작성) & 캐로셀
main_post_detail chatting makepost carousell
냠냠피드 수정 냠냠피드 삭제 나의 프로필
editfeedpost deletefeedpost myprofile
상대의 프로필 프로필 수정 맛집 등록
friendprofile editprofileinfo postplace
맛집 수정 맛집 리스트 & 별점순/최신순 필터 맛집 리스트 상세보기
editplacepost placelist filter placedetail
맛집 지도에서 보기 프로필 & 맛집 공유 드래그&드롭
place_kakaomap place_share_kakao 드래그앤드랍
맛집 길찾기 맛집 길찾기 로드뷰 에러 페이지
kakaomap_findroute_2 kakaomap_roadview error

9. 폴더 구조

📦src
├─📂api
├─📂components
│  ├─📂Carousels
│  ├─📂Chat
│  ├─📂Comment
│  ├─📂common
│  │  ├─📂Button
│  │  ├─📂Header
│  │  ├─📂Input
│  │  ├─📂Nav
│  │  └─📂Skeleton
│  ├─📂Error
│  ├─📂Feed
│  │  ├─📂FeedComment
│  │  ├─📂FeedCreate
│  │  ├─📂FeedHome
│  │  ├─📂FeedItem
│  │  ├─📂FeedList
│  │  └─📂ImgPrev
│  ├─📂FollowItem
│  ├─📂Loading
│  ├─📂Login
│  ├─📂Map
│  ├─📂Modal
│  │  ├─📂Alert
│  │  ├─📂Modal
│  │  └─📂PlaceCard
│  ├─📂Place
│  ├─📂Profile
│  ├─📂ProfileEdit
│  ├─📂ProfileSetting
│  ├─📂Search
│  ├─📂Signup
│  └─📂style
├─📂images
│  └─📂chatMembers
├─📂pages
│  ├─📂Chat
│  ├─📂Error
│  ├─📂Feed
│  ├─📂FollowerList
│  ├─📂Home
│  ├─📂Login
│  ├─📂Place
│  ├─📂Profile
│  ├─📂ProfileSetting
│  ├─📂Search
│  ├─📂SignUp
│  ├─📂Splash
│  └─📂Welcome
├─📂recoil
└─📂routers

10. 작업 문화

스크럼

☀️ Daily - 평일 오전 9시(약 15분 내외)

  • 어제의 활동 내용 요약
  • 오늘의 일정 계획
  • 어려움 또는 문제 발생 시 논의

✨ Weekly - 토요일 오전 11시

  • 일주일 동안의 진행 상황 공유
  • 지연된 사항 파악 및 계획 조정
  • 필요없다고 판단되는 프로세스 협의 및 개선 방안 검토

👍 Code review - 평일 오후 4시

  • 코드 공유 후 서로 피드백
  • 수정이 필요한 부분 도출 및 개선 계획 협의

라이브 쉐어

🧑‍💻 Microsoft Visual Studio Code의 Live Share 기능을 활용하여 오류 수정 시에도 페어 프로그래밍을 통해 팀원들 간의 효율적이고 원활한 의사소통을 유지합니다.

Git & 브랜치 전략

👉 Git Issue 작성 후 pr시 관련 Issue를 태그하여 커밋을 관리합니다.

👉 GitHub Flow

main : 배포가 될 브랜치입니다.

develop : 디폴트 브랜치입니다. 각자 브랜치 분기후 작업하여 충돌을 줄이고 안전하게 머지합니다.

팀 컨벤션/Convention

1.commit

예시)
(이모지)tag: subject
예시) git commit -m '✨feat: 새로운 기능 추가 #'
태그 (Tag) 제목 (Subject)
:sparkles:feat: 기능 추가, 삭제, 변경
🐛:bug:fix: 버그, 오류 수정
📝:memo:docs: readme.md, json 파일 등 수정, 라이브러리 설치 (문서 관련, 코드 수정 없음)
💄:lipstick:style: CSS 등 사용자 UI 디자인 변경, 코드 formatting, 세미콜론 누락, 코드 자체의 변경이 없는 경우
:white_check_mark:test: 테스트 코드, 리팩토링 테스트 코드 추가
📦️:package:chore: 패키지 매니저 수정, 그 외 기타 수정 ex) .gitignore
🚚:truck:rename: 파일 또는 폴더 명을 수정하거나 옮기는 작업만인 경우
💡:bulb:comment: 필요한 주석 추가 및 변경
🔥:fire:remove: 파일을 삭제하는 작업만 수행한 경우
👽️:alien:change: API 변경의 경우
🚑:ambulance:hotfix: 급하게 치명적인 버그를 고쳐야 하는 경우
♻️:recycle:refactor: 코드 리팩토링
🌱:seedling:add: 파일 추가
  1. Pull Request
### 💡 관련 이슈
<!-- - #이슈번호 -->

### ✍️ PR 한 줄 요약

### ✏ 상세 작업 내용

### ⭐ 참고 사항

### ✅ PR 양식 체크리스트
-[ ] 🔀 PR 제목의 형식을 잘 작성했나요? e.g. ✨feat: PR 등록
-[ ] 🧹 불필요한 코드는 제거했나요?
-[ ] 💭 이슈는 등록했나요?
-[ ] 🏷️ 라벨은 등록했나요?
  1. Branch
dev > feat/button/KM

도구

eslint - Linter. 소스 코드에 문제가 있는지 검사하여 문제가 있는 부분에 Flag를 달아주는 소프트웨어 도구
prettier - formatter. 소스 코드를 일관된 스타일로 작성할 수 있게 코드를 변환해주는 소프트웨어 도구

11. 후기

승규 : 처음 맡게 된 프로젝트팀장이였고 부족한 점이 많았지만 능숙한 팀원분들 덕분에 무사히 프로젝트를 끝마칠 수 있었습니다. 같이 하나의 서비스를 구현하는 과정에서 많은 어려움이 있었지만 해결하는 과정에서 팀으로도, 개인적으로도 성장할 수 있었던 좋은 기회였습니다!
수민 : 리액트 스킬, 팀 협업 경험, 문제 해결 능력, 그리고 사용자 피드백을 통한 성취감을 얻을 수 있는 좋은 경험이었습니다!
모건 : 이번 팀 프로젝트를 통해서 현재 내 위치를 가늠할 수 있었고, 함수형 컴포넌트 개발에 친숙해질 수 있는 좋은 기회였습니다. 팀 분위기가 좋아서 또 다른 팀 프로젝트도 해보고 싶어요. 팀원 여러분 감사합니다.
현지 : 여러 난관에 부딪혀 고민하고 해결하면서 스스로 많이 성장한 게 느껴지는 값진 시간이었습니다.