Search

소소(souso) - 중년 세대를 위한 커뮤니티 서비스

Tags
Javascript
React
MySQL
Created
2022.10 - 2022.12

프로젝트명

소소 (Souso) : 소년소녀들의 모임

참여기간

2022.10 - 2022.12

개발스택

React 18, Javascript
Axios, React-Query
styled-components
Java 11, Spring Boot, JPA, QueryDSL
MySQL, Redis
AWS, Docker, CI/CD, Nginx+Cerbot/SSL, Sonar Cloud

프로젝트 소개

It is a social networking platform for middle-aged adults to help them make neighborhood Friends easily. This app will track the user's location using KakaoMap API and will show postings from nearby users or chat with them. It was a part of Web Development challenges from NUMBLE, a side-project platform for developers in Korea, and my team won 2nd prize in this challenge!
Service Target Group: the middle aged like parents generation. (40~60)
Team Members

Demonstration Video

Main Features

1. Join Form - Phone number Verification

Implemented sending verification code by connecting backend API using React-Query.
Organized endpoints and queries for Join Page in a separated API folder.
Created a reusable Input component and useForm hook.
Code Preview
// api/queries/join.js import { api } from 'api'; import { JOIN, CHECK_NICKNAME, SEND_CODE, VERIFY_CODE } from 'api/endpoints'; export const join = { submit: async req => { const res = await api.post(JOIN, req); return res.data; }, nickname: async req => {...}, sendCode: async req => {...}, verifyCode: async req => {...} };
JavaScript
복사
// components/Join/InputVerified import React, { useState, useEffect } from 'react'; import { Input } from 'components/Join'; import { useMutation } from 'react-query'; import { join } from 'api/queries/join'; export const InputVerified = ({ values, onChange, errors, isVerified, setIsVerified}) => { const [isSent, setIsSent] = useState(false); // 👉 response from the verification code API const { mutate: sendCodeMutate } = useMutation(join.sendCode, { onSuccess: () => { errors.phone_number = ''; toast.success('Sent successfully'); setIsSent(true); setWaiting(true); }, onError: error => { if (error.response.data.message === 'Already Auth Code Exist') { errors.phone_number = 'You can send a code after 3mins.'; } else if (error.response.data.message === 'Already Phone Number Exist') { errors.phone_number = 'The phone number already exists.'; } } }); // 👉 request verification when button is clicked const sendCode = async e => { e.preventDefault(); const errorMessage = await validate(values); errors.phone_number = errorMessage.phone_number || ''; if (!errors.phone_number) { sendCodeMutate({ phone_number: values.phone_number }); } setRender(values); }; useEffect(() => { setIsSent(false); }, [setIsSent]); return ( <Input name="phone_number" placeholder="phone number" onChange={onChange} values={values} errors={errors} > <button onClick={sendCode}> {isSent ? 'Resend' : 'Send'} </button> </Input> // ... ); };
JavaScript
복사

2. KakaoMap API for Geolocation

When signing up, this API will help you to get the neighborhood name for the user's location.
 Read more about KakaoMap API
Adopted Recoil state storage to manage the neighborhood name globally.
Utilized Geolocation to get the current latitude and longitude of the user.
Code Preview
// components/TownAuth/KakaoMap import React, { useEffect, useRef, useState } from 'react'; import { Map, MapMarker } from 'react-kakao-maps-sdk'; import { useSetRecoilState } from 'recoil'; import * as S from './styles'; export const KakaoMap = ({ openModal }) => { const [currentGeo, setCurrentGeo] = useState({ lat: 0, lng: 0 }); const [pickedGeo, setPickedGeo] = useState(currentGeo); const [address, setAddress] = useState([]); const setSaveAddress = useSetRecoilState(addressState); const { kakao } = window; // 👉 call KakaoAPI from <head> const mapRef = useRef(); // 👉 get latitude and longitude of picked location on the map const getPickedGeo = (_, mouseEvent) => { setPickedGeo({ lat: mouseEvent.latLng.getLat(), lng: mouseEvent.latLng.getLng() }); }; // 👉 move the pin to the current location on the map. const moveToCurrent = () => { const center = new kakao.maps.LatLng(currentGeo.lat, currentGeo.lng); if (mapRef.current) { mapRef.current.panTo(center); setPickedGeo(currentGeo); } }; useEffect(() => { // 👉 get the current location using GeoLocation if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(({ coords }) => { setCurrentGeo({ lat: coords.latitude, lng: coords.longitude }); setPickedGeo({ lat: coords.latitude, lng: coords.longitude }); }); } }, []); useEffect(() => { const geocoder = new kakao.maps.services.Geocoder(); // 👉 request the detailed address of coordinates const getAddressFromGeo = (coords, callback) => { geocoder.coord2Address(coords.lng, coords.lat, callback); }; // 👉 return the address when the specific point is clicked on the map. getAddressFromGeo(pickedGeo, function (result, status) { if (status === kakao.maps.services.Status.OK) { setAddress([ result[0].address.region_1depth_name, result[0].address.region_2depth_name, result[0].address.region_3depth_name ]); } }); }, [kakao, pickedGeo, setAddress]); useEffect(() => { setSaveAddress(address); // save the address }, [setSaveAddress, address]); return ( <S.MapContainer> <Map center={currentGeo} onClick={getPickedGeo} className="kakao_map" ref={mapRef} > <MapMarker position={pickedGeo} /> </Map> <SearchSection openModal={openModal} moveToCurrent={moveToCurrent} /> </S.MapContainer> ); };
JavaScript
복사

3. Infinite Scroll useInfiniteQuery by React-Query

Implemented the infinite scroll when the feed list is loaded
If the scroll is down to the FetchObserver, it will send request to fetch the next page.
Code Preview
// pages/FeedPage import { useInfiniteQuery } from 'react-query'; import { feed } from 'api/queries/feed'; export const FeedPage = () => { const params = pageParam => { return { cursorId: isLatest ? pageParam : 0, pageId: !isLatest ? pageParam : 0, sortType: isLatest ? 'LATEST' : 'POPULAR' }; }; // request fetching more feed data if the next page exists. const infiniteResponse = useInfiniteQuery( ['feed'], ({ pageParam = 0 }) => feed.list(params(pageParam)), { getNextPageParam: lastPage => isLatest ? lastPage.feed_list.length > 0 && lastPage.feed_list.slice(-1)[0].feed_id : lastPage.page_id + 1 } ); return ( <PostList active={active} setActive={setActive} infiniteResponse={infiniteResponse} /> ); };
JavaScript
복사
// components/Feed/PostList export const PostList = ({ infiniteResponse, active, setActive }) => { const { data, isLoading, isFetching, fetchNextPage, refetch } = infiniteResponse; const { pathname } = useLocation(); const isEmpty = !isLoading && ('feed_list' in data.pages[0] ? !data.pages[0].feed_list.length : !data.pages[0].category_feed_list.length); if (isEmpty) return <EmptyList message="Empty Feed" />; return ( <S.PostListContainer> {active === 'popluar' && (isTabLoading ? ( <SkeletonThRight /> ) : ( <S.PostLists> {data.pages.map(page => (page.feed_list || page.category_feed_list).map(post => ( <ThumbRight key={post.feed_id} postData={post} /> )) )} </S.PostLists> ))} {/* Latest feed or categorized feed */} {active !== 'popluar' && (isTabLoading ? ( <SkeletonThBottom /> ) : ( <S.PostLists> {data.pages.map(page => (page.feed_list || page.category_feed_list).map(post => ( <ThumbBottom key={post.feed_id} postData={post} refetch={refetch} /> )) )} </S.PostLists> ))} {/* If the scrollbar scroll down to this, fetch next page */} <FetchObserver data={data} fetchNextPage={fetchNextPage} isFetching={isFetching} /> </S.PostListContainer> ); };
JavaScript
복사

4. Custom Skeleton UI

Created a skeleton component as placeholders when texts and images are loading.
Code Preview
// components/Common/Skeleton import React from 'react'; import { Skeleton } from './Skeleton'; import styled from 'styled-components'; export const SkeletonCategory = () => { return ( <Container> {[...Array(8)].map((_, i) => ( <div key={i}> <Skeleton type="circle" size={40} /> <Skeleton type="text" width={40} /> </div> ))} </Container> ); }; export const Skeleton = ({ type, height, width, size, line }) => { return [...Array(line)].map((_, i) => ( <SkeletonItem key={i} className={type} height={height} width={width} size={size} > <HighLight /> </SkeletonItem> )); };
JavaScript
복사

Project Proposal

메인으로 돌아가기