GithubHelp home page GithubHelp logo

lego-market's Introduction

레고 마켓

1. 프로젝트 소개

레고마켓 썸네일.png

1.1💡 프로젝트 명: 레고마켓

🔗 배포 URL: https://fir-project-3397b.web.app

🟥  SNS와 마켓 서비스를 제공합니다.

🟨  레고를 판매, 홍보할  있습니다.

🟩  팔로우를 통해 게시물을 구경하며, 원하는 게시물에 '좋아요' 누르거나 댓글을 달수 있습니다.

🟦  레고가 없어도 플랫폼을 이용할  있습니다.

1.2 개발 환경

⚙️  사용기술

배포

🔎  이슈 관리

2. 팀원 소개

팀원.PNG

3. 기능구현 및 역할 분담

역활분담

4. 핵심기능 시연

splash 회원가입
splash.gif 회원가입후로그인 2.gif
로그인 로그아웃
로그인 로그아웃
프로필수정 상품등록
프로필수정 상품등록
게시글등록 채팅
게시글등록 채팅
유저검색 팔로우
유저검색 팔로우
상품삭제 게시글삭제
상품삭제 게시글삭제

4. 폴더 구조

📦Lego-Market
  📂 public
   📂 images           // 이미지 파일 폴더
   📂 icons            // 아이콘 파일 폴더
   index.html
  📦 src
   📂 components       //스타일도 분리했기 때문에 각각 용도별로 폴더를 나눠서 관리
    📂 addproduct
    📂 chat
    📂 comment
    📂 follow
    📂 home
    📂 join
    📂 login
    📂 modal
    📂 post
    📂 postdetail
    📂 postModify
    📂 postupload
    📂 productlist
    📂 profile
    📂 search
    📂 ui             // 공통으로 사용되는 컴포넌트 폴더
   📂 context          // 로그인 한 사용자 정보를 담기 위한 context 파일 관리 폴더
   📂 hooks            // 만들어서 사용한 훅 폴더
   📂 pages            // 페이지 컴포넌트
   📂 routes           // 라우터 파일 관리
   📂 styles           // 전역 styled-component 관리
   📜 App.jsx
   📜 Portal.js
   📜 index.js
  📜 .gitignore
  📜 README.md
  📜 package-lock.json
  📜 package.json

5. 주요코드

5.1 페이지마다 있는 UI 재사용 하도록 구현


상단 네비게이션 바인 TopNav와 재사용 예

function TopNav(props) {
    const { leftChild, centerChild, rightChild } = props;

    return (
        <Wrapper>
            <Left>{leftChild}</Left>
            <Center>{centerChild}</Center>
            <Right>{rightChild}</Right>
        </Wrapper>
    );
}

Home페이지에서 재사용한 TopNav 컴포넌트

<TopNav
    leftChild={<HeaderStrong>레고마켓피드</HeaderStrong>}
    rightChild={
        <HomeHeaderImg
            src={process.env.PUBLIC_URL + "/icons/icon-search.png"}
            alt="피드찾기"
            onClick={() => navigate("/search")}
        />
    }
/>

home피드상단

Search페이지에서 재사용한 TopNav 컴포넌트

<TopNav
    leftChild={<BackButton />}
    centerChild={<SearchInput placeholder="계정검색" onChange={onChange}></SearchInput>}
/>

search페이지 상단

5.2 context를 사용하여 로그인 한 사용자 정보를 관리

로그인 요청 또는 프로필 수정 시 서버에서 오는 데이터를 context에 저장해서 Props Driling을 피하고 어느 컴포넌트에서든 로그인한 유저의 정보를 사용할 수 있게 하였습니다.

import { createContext, useReducer } from "react";

const AuthContext = createContext();

const authReducer = (state, action) => {
    switch (action.type) {
        case "login":
            return { ...state, user: action.payload };
        case "modify":
            return { ...state, user: action.payload };
        default:
            return state;
    }
};

const AuthContextProvider = ({ children }) => {
    const [state, dispatch] = useReducer(authReducer, {
        user: null,
    });

    return (
        <AuthContext.Provider value={{ ...state, dispatch }}>
            {children}
        </AuthContext.Provider>
    );
};

export { AuthContext, authReducer, AuthContextProvider };
import { useContext } from "react";
import { AuthContext } from "../context/AuthContext";

export const useAuthContext = () => {
    const context = useContext(AuthContext);
    return context;
};

저장해둔 user 정보를 프로필 페이지에서 사용한 경우입니다.

import React from "react";
import styled from "styled-components";
import { useParams } from "react-router-dom";
import { useAuthContext } from "../../hooks/useAuthContext";

import Profile from "../../components/profile/profile/Profile";
import ProfilePost from "../../components/post/profilePost/ProfilePost";
import ProductList from "../../components/productlist/ProductList";

const ProfileMainWrap = styled.div`
    min-width: 390px;
    width: 100%;
    height: calc(100% - 61px);
    overflow-y: auto;
    overflow-x: hidden;
`;

function ProfilePage() {
    const { accountname } = useParams();
    const { user } = useAuthContext();

    let profileAccountName;
    let myAccountName;
    if (user) {
        myAccountName = user.accountname;

        accountname === undefined
            ? (profileAccountName = myAccountName)
            : (profileAccountName = accountname);
    }

    return (
        <ProfileMainWrap>
            <Profile
                profileAccountName={profileAccountName}
                myAccountName={myAccountName}
            />
            <ProductList
                profileAccountName={profileAccountName}
                myAccountName={myAccountName}
            />
            <ProfilePost profileAccountName={profileAccountName} />
        </ProfileMainWrap>
    );
}

export default ProfilePage;

5.3 API 요청

비슷한 API 요청들을 훅으로 구현해서 사용하는 방법과 요청들을 하나의 파일에 모아서 사용하는 방식 두 가지로 진행했습니다. 이건 여러 방법들을 시도해 보자는 학습적인의도로 진행했습니다.

요청을 모아서 사용하는 방법에선 아래와 같이 API 주소를 기본값으로 설정해서 따로 입력하지 않도록 했습니다. 또 요청을 하는 시점에 토큰 값을 조회해서 헤더에 넣어주도록 했습니다.

// /src/componenets/hooks/useAxios.jsx 파일
import axios from "axios";
// baseURL 설정
axios.defaults.baseURL = "https://mandarin.api.weniv.co.kr";
axios.defaults.headers["content-Type"] = "application/json";
// 요청하기전에 헤더에 토큰 추가
axios.interceptors.request.use(
    function (config) {
        config.headers["Authorization"] = `Bearer ${localStorage.getItem("token")}`;
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);

요청 메서드와 경로를 넣어서 값을 반환하는 함수를 하나의 요청마다 만들어서 사용했습니다.

// 팔로우한 게시글 불러오기
export const getFeedPost = async () => {
    console.log("getFeedPost-Called");
    const config = {
        method: "GET",
        url: `/post/feed`,
    };
    try {
        const response = await axios.request(config);
        return response;
    } catch (error) {
        console.log(error);
    }
};

만든 함수는 아래와 같이 필요한 컴포넌트에서 import 해서 async-await 로 반환값을 state 변수로 관리해 사용 했습니다.

import { getFeedPost } from "../../hooks/useAxios";
....

const Home = () => {
    const [postData, setPostData] = useState();
    const navigate = useNavigate();

    useEffect(() => {
        (async () => {
            const res = await getFeedPost();
            setPostData(res);
        })();
    }, []);

유사한 요청을 훅으로 만들어 사용한 경우입니다. 대표적으로 댓글, 게시물, 상품의 삭제 요청이 동일해서 재사용하면 좋을 거 같아 아래와 같이 구현했습니다.

import { useState } from "react";

export const useDelete = () => {
    const [isUpdate, setIsUpdate] = useState(false);
    const remove = async (addUrl) => {
        try {
            const token = localStorage.getItem("token");
            const serverUrl = "https://mandarin.api.weniv.co.kr";
            const url = serverUrl + addUrl;

            const res = await fetch(url, {
                method: "DELETE",
                headers: {
                    Authorization: `Bearer ${token}`,
                    "Content-type": "application/json",
                },
            });
            const json = await res.json();
            console.log(json);
            setIsUpdate(!isUpdate);
        } catch (error) {
            console.log(error);
        }
    };
    return { remove, isUpdate, setIsUpdate };
};

댓글삭제 기능에서 아래와 같이 사용했습니다.

const [deleteAlertModal, setDeleteAlertModal] = useState(false);

...

const deleteAlertButton = {
        content: "삭제",
        onClick: () => {
            remove(`/post/${post_id}/comments/${comment_id}`); 
            // 클릭 시 삭제요청
            setModal(false);
            setDeleteAlertModal(false);
   },
};

5.4 하단 네비게이션 바

하단 네비게이션 바를 만들었는데 필요한 페이지, 필요하지 않은 페이지들이 있어서 필요한 컴포넌트에만 넣어서 사용해야 하나 고민하다가 라우팅중첩을 이용해 아래처럼 필요한 페이지들만 묶어서 구현했습니다.

const WithNav = () => {
  return (
      <>
          <NavBar />
          <Outlet />
      </>
  );
};
export default WithNav
<Route element={<WithNav />}>
    <Route path='/home' element={<Home />} />
    <Route path='/search' element={<SearchUserPage/>}/>
    <Route path='/myprofile' element={<ProfilePage/>} />
    <Route path='/myprofile/:accountname' element={<ProfilePage/>} />
    <Route path='/follow/:accountname/:type' element={<FollowPage/>}/>
    <Route path='/editpost' element={<PostUploadPage/>} /> 
    <Route path='/chat/list' element={<ChatPage/>} />
    <Route path='/chat/:id' element={<Chat/>} />                                     
    <Route path='/productlist' element={<ProductListPage/>} />                 
    <Route  path='/product'  element={<AddProductListPage/>} />                                   
</Route>

6. 이슈 및 해결

  1. 게시글 삭제 “ 왜 바로 삭제 안돼,,”

    • 게시글을 삭제 시 삭제가 바로 반영이 되지 않아서 window.location.onload() 로 새로고침을 강제로 해줬습니다. 그런데 삭제할 때마다 딜레이가 되어 문제가 발생
    • 고민하다가 삭제 버튼을 누르는 시점에 유저의 데이터를 가져오는 컴포넌트(ProfilePost)에 데이터를 갱신하도록 refetch state 변수를 설정하여 해결
  2. SearchUser 페이지 “키워드를 입력하기 전에 결과 뜨지 않도록 하기”

    • SearchUser 페이지에 들어갔을 때 키워드를 입력하지 않아서 모든 유저들을 불러오는 문제가 있었는데 전달하는 값이 “ ” 빈 값일 때 값을 반환하지 않도록 false를 넣어줘서 해결했습니다.
    export const searchUser = async (keyword) => {
        const config = {
            method: "GET",
            url: `/user/searchuser/`,
            params: {
                keyword: keyword === "" ? false : keyword,
            },
        };
        try {
            const response = await axios.request(config);
            return response;
        } catch (error) {
            console.log(error);
            throw error;
        }
    };
  3. headers 오타 “왜 에러가 안 날까?”

    • api 요청 시 headers 설정에서 “**s”**를 넣지 않았는데 에러 표시가 뜨지 않아서 오류를 찾는데 많은 시간이 지체되었던 점.

7. 프로젝트 후기

아쉬운점

  • 프로젝트 초반 깃 활용 어려움으로, 깃 플로우 전략 늦게 사용 (main - develop - feature 3가지만 써서 제대로 사용하진 못했다)
  • 잦은 깃 충돌로 인해 많은 시간 지체
  • 기능 구현을 최우선으로 진행하여 문서화를 자세하게 하지못함
  • 프로젝트 막바지에 스타일 컴포넌트 분리 작업

잘한점

  • 99% 비대면 회의 진행: 수업 후, 매주 평일 2시간씩 회의하며 작업 내용 공유 및 일정 관리
  • 매일 서로 해결되지 않는 문제들을 공유하며 함께 문제 해결, 리액트 스터디를 진행하여 개념 이해

계획

  • 8월 둘째 주까지 추가적으로 남은 기능 구현(게시물수정, 신고)와 최적화를 위해 계속해서 업데이트 예정 최적화 중에 큰 용량의 이미지을 받았을 때 이미지를 리사이징해서 사용하는 방법을 먼저 적용해 보고 싶습니다.

lego-market's People

Contributors

wonhyeonglee avatar hyebin-woo avatar leehyeonseop avatar jamgoori avatar

Stargazers

HYEBIN KIM avatar Daeyeob Kim avatar

lego-market's Issues

refactoring ♻️: [프로필이미지, 유저닉네임, 유저아이디] 로 이루어진 컴포넌트 생성

리팩토링 할 곳/리팩토링이 필요한 이유

SearchUser의 검색결과 부분을 따로 빼서 [프로필이미지, 유저닉네임, 유저아이디] 로 이뤄진 컴포넌트로 만들어 다른곳에서 사용할 수 있게 만들면 재사용할 수 있을 것 같습니다.

리팩토링 계획
재사용 할 수 있다고 생각되는 부분들은 다음과 같습니다

  1. Home 의 팔로우한 사람들의 피드위
  2. Search페이지 키워드 검색결과
  3. Profile 의 게시글 부분
  4. Post 부분

리팩토링 후 예상 개선결과

좀 더 개선하면 6개의 부분에서도 재사용할 수 있을 것 같습니다.

feat ✨ 커스텀 useAxios 훅 구현

구현할 기능

axios.request(config) 요청을 이용해서 컴포넌트에서 필요한 요청에 따른 경로, 요청, 값을 전달하면 결과값을 반환하는 기능
isPending 값이나 intercepter 기능 이용해서 전송중엔 로딩표시가 뜨도록 처리하는 기능

구현방법 | 예상 동작

useAxios 내부에서 dafault URL, headers 를 설정해놔서 요청할 때 헤더를 다시 쓰지 않아도 되도록 구현합니다
사용하는 컴포넌트에서 config 로 구성해서 넘겨서 axios.request(config) 으로 작동되도록 구현합니다
ㄴ 이유 : 훅에서 axios.get , axios.post 처럼 사용하는 것보다 config 를 받아서 처리하는게 더 깔끔해보여서

특이사항

사용까진 hook 을 만들어서 분리했으니 API 를 어떻게 한 곳에 모을지 생각 해보겠습니다

feat ✨ 피드없는 상태의 Home 과 하단 NavBar 구현 + 기존 Splash css 수정

구현할 기능

API 요청 후 Post 가 없을 때는 기본페이지가 있을때는 Post 데이터와 함께 Feed 페이지가 렌더되도록 구현
NavBar 는 NavBarItem 컴포넌트를 만들어서 개별로 생성하도록 구현(props 로 경로, 텍스트, 활성아이콘, 비활성아이콘 전달)
useLocation 을 사용해서 이동하는 경로와 pathname 이 일치할 시 활성아이콘 ( 색이 들어감 ) 을 보여주도록 구현
기존 가운데만 보여있고 중앙정렬이 이상하게 들어가있던 Splash.jsx 의 CSS 부분 영억을 전체로 , Home 페이지 CSS 중앙정렬 수정

구현방법 | 예상 동작

splash수정

홈페이지

특이사항

필요한 아이콘 파일들 /public/icons 에 추가합니다

feat ✨ 팔로워 팔로우 리스트 목록 기능 구현

구현할 기능

스크린샷 2022-07-22 오후 3 01 06

구현방법 | 예상 동작

  • 내 프로필에서 팔로워, 팔로우 버튼 클릭시 팔로워, 팔로우 목록 구현
  • 팔로워 팔로우 목록 프로필 클릭시 프로필로 이동
  • 버튼의 (팔로우,언팔로우) 변화만 구현

특이사항

  • 다음 도전과제로 팔로우, 언팔로우 기능 구현하기

feat ✨ 채팅방 페이지 구현

구현할 기능

채팅방에 메세지를 입력(현재 시각)

구현방법 | 예상 동작

댓글 컴포넌트와 비슷하게 inputFooter 컴포넌트 재활용

특이사항

마크업 구현 및 스타일 적용

feat ✨ 로그아웃 기능 구현

구현할 기능
--- 로그아웃 기능 구현

구현방법 | 예상 동작
--- 로그아웃 버튼을 클릭하면 토큰이 사라질고 splash 로 이동합니다.

특이사항

feat ✨ 프로필 페이지, 프로필 수정 페이지

구현할 기능
--- accountname에 맞는 프로필 페이지
--- 프로필 수정페이지

구현방법 | 예상 동작
--- accountname에 맞는 사용자의 프로필 페이지 구현 및 프로필 수정

특이사항

feat ✨ postupload 페이지 이미지 업로드 & 삭제 구현

구현할 기능

게시물 작성페이지에 이미지를 최대 3장까지 업로드
이미지 오른쪽에 삭제 버튼 클릭시 이미지 삭제

특이사항

앞으로 추가해야할 작업

  • 2장 이상 업로드시 이미지 크기 줄이기
  • 피드 게시글 업로드 구현

refactoring ♻️: 에러처리 추가

리팩토링 할 곳/리팩토링이 필요한 이유
--- 모든 커스텀 훅 및 login join 컴포넌트의 api 요청시 에러처리

리팩토링 계획
--- 에러가 발생했을때 적절한 처리

리팩토링 후 예상 개선결과

feat ✨상품클릭시 상황별 다른 반응

구현할 기능
--- 본인 프로필인지 상대방 프로필인지 구분해서 상품클릭시 다른 반응

구현방법 | 예상 동작
--- 본인 프로필에서 상품 클릭시 모달창이 뜨면서 상품 삭제, 수정이 가능함.
타인의 프로필에서 상품 클릭시 모달창이 뜨지않고 바로 해당링크로 이동

특이사항

feat ✨ 회원가입 페이지

구현할 기능
--- 회원가입 페이지 이메일검증, 이미지 등록, 계정ID 검증

구현방법 | 예상 동작

특이사항

feat ✨ 토큰검증

구현할 기능
--- 얻어온 토큰이 유효한 값인지 검증합니다.

구현방법 | 예상 동작
--- 유효한 토큰인지 확인하여 적절한 라우팅 처리를 합니다.

특이사항

refactoring ♻️: 프로필수정시 원래 정보 유지

리팩토링 할 곳/리팩토링이 필요한 이유

프로필 수정페이지에서 원래 프로필정보를 유지합니다.

리팩토링 계획

프로필 수정페이지에서 원래 프로필정보를 유지합니다.

리팩토링 후 예상 개선결과

💄 LoginModal 스타일 컴포넌트 적용해서 전환

구현할 기능
LoginModal 컴포넌트 style-components 로 바꾸기

구현방법 | 예상 동작
그대로 이미지 스프라이트 사용해서 좌표따라 다른 아이콘 나오도록 구현
SVG 스프라이트 기법도 있던데... 몇번봐도 이해를 못하겠어서 포기

feat ✨404페이지

구현할 기능
---404페이지를 제작합니다.

구현방법 | 예상 동작

404

특이사항

feat ✨ Splash 페이지 추가

구현할 기능

기본 로딩 페이지 기능을 하는 Splash.jsx 입니다
- 로그인 상태가 아니면(초기값)
로그인 모달창이 밑에서 나옵니다
- 로그인상태면
1.5초 후 Home 페이지로 이동합니다

구현방법 | 예상 동작

로그인 상태인지 아닌지 판단은 localStorage에 토큰이 있는지 없는지를 이용했습니다

Token없을때작동

Token있을때Home으로이동

특이사항

테스트를 위해 토큰을 localStorage에 저장하기 위한 버튼이 있습니다. ( 로그인, 회원가입 과 연결 후 삭제할 예정입니다)
스타일 높이를 수정해야합니다.
일단 token 값을 navigate 에 props 를 전달하는 방식으로 넘겨줬습니다.

refactoring ♻️: 토큰이 유효할때 프로필정보 요청 및 저장

리팩토링 할 곳/리팩토링이 필요한 이유
--- 토큰이 있는경우 프로필정보를 요청 및 저장 => 토큰의 유효성체크과정 추가

리팩토링 계획
--- 토큰이 존재하고 또한 그 토큰이 유효할 때 프로필정보를 요청한 후 저장합니다.

리팩토링 후 예상 개선결과

feat ✨ 댓글 신고 기능 및 모달창 구분

구현할 기능

(로그인한 작성자 제외) 댓글 더보기 버튼을 클릭시 신고하기 모달창 띄우기
댓글 신고 기능 구현

구현방법 | 예상 동작

신고를 누르면 alert창이 나오고, api 응답 값으로 신고된 댓글 id가 나옵니다.

특이사항

feat ✨ 댓글삭제 및 모달 구분

구현할 기능

댓글의 더보기 버튼 클릭시 삭제 기능 구현
댓글작성자와 로그인한 작성자가 같다면 삭제 모달 창, 댓글 작성자와 로그인한 작성자가 다르다면 신고 모달 창 구현.

구현방법 | 예상 동작

  • 전어 더보기 클릭시 신고
    스크린샷 2022-07-27 오후 8 51 26

  • 감규리 더보기 클릭시 삭제
    스크린샷 2022-07-27 오후 8 51 53
    스크린샷 2022-07-27 오후 8 52 41

-삭제 완료
스크린샷 2022-07-27 오후 8 53 13

특이사항

댓글 신고 기능 구현 예정

feat ✨ 좋아요 기능 추가

구현할 기능

좋아요, 좋아요취소 기능

구현방법 | 예상 동작

/post/:post_id/heart 에 요청하고 hearted 값으로 on/off 를 식별, heartCount 로 카운팅 취소도 동일하게 구현

feat ✨ 댓글 컴포넌트 기본 기능 구현

구현할 기능

기본 댓글 컴포넌트 기능 구현

구현방법 | 예상 동작

  • 댓글 리스트 화면 추가
  • 댓글 작성 api 추가
  • inputFooter 입력시 버튼 활성화

특이사항

  • 테스트 아이디와 임시 토큰을 사용하여 추후 삭제 예정
  • 나중에 useContext로 user.image 받아와 inputFooter의 img에 넣을 예정

Bug🐛 프로필 페이지 로딩버그 및 프로필 원래계정ID 버그 수정

버그 설명 (스크린샷 )
--- 원래의 계정ID 로 프로필 수정을 못했던 버그 수정
프로필페이지 새로고침시 로딩페이지 먼저 나오게 수정

수정계획
--- 원래의 계정ID 로 프로필 수정을 못했던 버그 수정
프로필페이지 새로고침시 로딩페이지 먼저 나오게 수정

수정완료 후 예상동작

특이사항

feat ✨ useLogin 훅, useProfile 훅

구현할 기능
--- 로그인 시 유저정보를 context에 저장하는 useLogin 훅 프로필데이터를 불러오는 useProfile 훅

구현방법 | 예상 동작
--- 유저정보를 context에 저장합니다.
--- profile 정보를 마운트시에 한번만 생성합니다.

특이사항

feat ✨ 회원가입 페이지

구현할 기능

이메일로 회원가입

구현방법 | 예상 동작

이메일과 비밀번호를 입력받아 회원가입 구현

특이사항

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.