📍 Website link
📺 YouTube link_1 /
YouTube link_2
✨Team Members
- Login
- Signup
- Home Page
- Detail Page
- Product Registration Page
- Mypage + MyStore
- Chatting
-
오타쿠의, 오타쿠에 의한, 오타쿠를 위한 경매사이트 OKU는 오타쿠들의 니즈를 충족시켜줄 웹서비스입니다.
내가 좋아하는 분야의 굿즈를 좋은 가격에 팔 수 있고, 그동안 돈 주고도 못샀던 굿즈들도 OKU에서는 구할 수 있습니다!
- Design: UI/UX 2인 (남유진, 이소희)
- Backend: Node.js 2인 (김연재, 원가연) [Repo가기👉]
- Frontend: React.js 3인 (정성목, 최경민, 최용현)
- axios, fetch
- socket.io / moment.js / react-responsive
- redux-middleware(redux-thunk)
- connected-react-router, history
- react-redux, redux (+ redux-actions, immer 사용)
- View: React with JavaScript, Material-UI,styled-components
- Infrastructure: AWS S3 버킷 생성, Route 53을 이용한 도메인 네임서버 등록, Amazon CloudFront??, ACM 보안 인증서 발급 및 등록
- Git, Github, Notion, Zeplin, Slack, Google Drive etc.
-
디자인 시스템에 맞추어 시스템 및 element 컴포넌트화 ex. button, input, etc.
- 로그인(+카카오로그인), 회원가입
- Chatting : 1대1, 전체 채팅
- Home Page : MD 추천 상품, 인기 상품, 마감임박 상품, 알림, Carousel
- Detail Page : 마감시간, 입/낙찰, 실시간 입찰 정보, 댓글 작성, 좋아요, Carousel
- Product Registration Page : 이미지 미리보기, 주소 찾기
- Mypage + MyStore : 회원정보 수정, 내 상점 관리, 좋아요 리스트, 판매 상품 리스트
- Moment.js 라이브러리를 이용한 실시간 상품 마감시간 구현
- 실시간 입찰 정보 : 현재 입찰 정보(입찰자+입찰금액)를 실시간으로 확인 가능, 본인이 제시한 입찰 금액 즉시 확인 가능
- 입찰 시 현재 입찰가 초과, 즉시 낙찰가 미만의 입찰금액만 제시 가능
- Socket.io를 통한 1:1 채팅 및 전체 채팅 구현
-
PC + Mobile
-
Media Query
-
React Hook : useMediaQuery 사용
useMediaQuery 사용 방법
- react-responsive 설치
import { useMediaQuery } from "react-responsive";
const Desktop = ({ children }) => { const isDesktop = useMediaQuery({ minWidth: 1024 }); return isDesktop ? children : null; }; const Tablet = ({ children }) => { const isTablet = useMediaQuery({ minWidth: 768, maxWidth: 1023 }); return isTablet ? children : null; }; const Mobile = ({ children }) => { const isMobile = useMediaQuery({ maxWidth: 767 }); return isMobile ? children : null; };
- 컴포넌트 생성 후 import하여 사용하는 방법
import React from "react"; import { useMediaQuery } from "react-responsive"; export const Mobile: React.FC = ({ children }) => { const isMobile = useMediaQuery({ query: "(min-width:0px) and (max-width:599px)", }); return <React.Fragment>{isMobile && children}</React.Fragment>; };
import React from "react" const { Mobile } from "../mediaQuery" export const Main = () => { return ( <div>hello</div> <Mobile> <div>hi</div> </Mobile> ) }
-
스크롤 반응형 헤더
-
사용자 편의성을 위해 스크롤에 따른 헤더 높이 조절 + 로고 위치 및 크기 변경
-
-
알림
-
낙찰 시 판매자 알림 : 상대방의 거래 요청 수락 or 거절 가능
-
거래 요청이 수락된 구매자의 알림
-
-
카테고리 검색
- 대분류 선택 시 대분류에 해당하는 중분류 선택이 가능하도록 구분
-
일반 검색
-
일반 로그인(이메일, 비밀번호) : JWT
-
아이디 저장 기능 : localStorage에 영구 저장
-
소셜 로그인(카카오로그인)
-
아이디(이메일)와 닉네임 중복 및 정규식 체크
-
비밀번호 정규식 체크, 비밀번호 확인
-
[ CRUD ] : 실시간 인기 상품 / 최신 등록상품리스트 / 마감 임박 상품 / MD 추천 상품
-
[ Carousel ]
-
홈페이지 렌더링 시 처음으로 보여지는 실시간 인기 상품 리스트를 Carousel로 구성
-
React-Slick을 사용
-
-
[ 카드 ]
-
마감 기한, 상품 제목, 최소 입찰가 정보 제공
-
좋아요 + Timer 기능 구현
-
-
[ CRUD ] : 상품 등록
-
[ 이미지 미리보기 ]
- 이미지 업로드 최대 3장으로 제한 -> 개별 이미지 업로드 버튼(handleChange1, 2, 3) 구현
const handleChange1 = (e) => { const reader = new FileReader(); const file = fileInput.current.files[0]; // 파일 내용을 읽어오기 reader.readAsDataURL(file); // 읽기가 끝나면 발생하는 이벤트 핸들러 reader.onloadend = () => { dispatch(uploadActions.setPreview1(reader.result)); }; };
- 이미지 미리보기 Redux
// actions const SET_PREVIEW_1 = "SET_PREVIEW_1"; // action creators const setPreview1 = createAction(SET_PREVIEW_1, (preview) => ({ preview })); // reducer export default handleActions( { [SET_PREVIEW_1]: (state, action) => produce(state, (draft) => { draft.preview1 = action.payload.preview; }) }, initialState );
-
[ 경매 기간 ]
- millisecond로 변환 후 서버에 전달 ex. 3시간 = 10800000, 1일 = 86400000
const [deadline, setDeadline] = useState(""); const handleDeadline = (e) => { setDeadline(parseInt(e.value)); }; - 생략 - <Select onChange={handleDeadline} value={D4CT.find((obj) => obj.value === deadline)} placeholder="경매 기간" options={D4CT} />
-
[ 주소 검색 ]
- 희망 거래 방식 중 '직거래' 클릭 시 주소 검색 항목 생성
- Daum Postcode 라이브러리 사용
// 우편번호 / 주소 찾기 const handleComplete = (data) => { let fullAddress = data.address; let extraAddress = ""; if (data.addressType === "R") { if (data.bname !== "") { extraAddress += data.bname; } if (data.buildingName !== "") { extraAddress += extraAddress !== "" ? `, ${data.buildingName}` : data.buildingName; } fullAddress += extraAddress !== "" ? ` (${extraAddress})` : ""; } setRegion(fullAddress); // 주소 모달 닫기 setIsPostOpen(false); };
-
[ CRUD ] : 상품 상세 정보 / 입찰 / 낙찰 / 찜(좋아요) / 문의하기
-
[ Carousel ]
- 이미지 최대 3장으로 제한
- margin 값 조정을 통한 Carousel 기능 구현
- 좌/우 버튼 클릭 시 margin 값이 조정되어 웹에서는 원활히 작동하지만, 모바일에서는 스크롤이 되지 않음
- 확장 가능한 Carousel 구현을 위해 [ 스크롤 + 스냅 ] 활용 예정
-
[ Timer ]
- useInterval hook / moment / 내장 시간 함수를 통한 Timer 구현
- useInterval을 통해 1000ms 단위로 현재 시간 표현
- 시간을 나타내는 '타이머'의 경우 -> [ 마감시간-현재 ]
- 프로그레스 바와 같은 진행상태를 나타내는 '스탑워치'의 경우 -> [ 마감시간-(현재-시작시간) ]
- 고민 내용
- 서버와의 소통을 위한 시간의 형태(timestamp or isoDate) 고민
-> 상대적으로 가독성이 높은 isoDate으로 소통 - 프로그레스 바 구현 위해 어떻게 시간차를 숫자값으로 인식하게 할 수 있을지 고민
-> moment 라이브러리의 difference를 이용하면 계산 가능한 숫자값으로 반환하지 않기에 내장함수 ?? 이용
- 서버와의 소통을 위한 시간의 형태(timestamp or isoDate) 고민
-
[ 입찰 & 낙찰 ]
-
Modal 컴포넌트화 + useInterval을 통해 제한적 통신 요청 및 실시간 같은 UX 구현
-
입찰표 작성 Modal
- 현재 최고 입찰가와 마감시간 확인 + 입찰 시도 가능
- 입찰 후 곧바로 입찰을 재시도할 수 있기에 입찰 시도 후에도 모달 유지
-
고민 내용
- 웹소켓을 이용한 양방향 통신을 기획했으나 입찰과 낙찰의 경우 일방적인 요청이므로 웹소켓이 필수가 아닌 것으로 판단
- 빠른 기능 구현을 위해 useInterval을 이용하여 현재 입찰가 및 onSale(경매중인지 아닌지 여부) 구현
- 추후 웹소켓을 이용한 기능 구현 예정
-
-
[ 문의하기 & 대댓글 ]
- 문의하기는 구매자와 판매자 모두 가능하지만 문의글에 대한 답글은 판매자만 가능
- 대댓글 구현을 위해 문의글 마다 고유 ID 지정
-
Mypage
-
[ CRUD ] : 회원정보(+수정) / 찜 리스트 / 구매 리스트
-
-
MyStore
-
[ CRUD ] : 내 상점(+수정) / 내 상점 후기 / 현재 판매 상품
-
현재 판매 중인 상품이 없는 경우 물건 등록 버튼 생성
-
- Socket.io 라이브러리 사용 -> 클라이언트와 서버의 양방향 통신 가능
- 채팅 페이지 이동 시 서비스 이용자들과의 전체 채팅(아무나 채팅) 가능
- 판매자와 구매자의 거래가 성사되었을 경우에만 1:1 채팅 가능 -> 거래자 채팅 목록 안에 채팅방 생성, 거래 종료 및 주소 검색 가능
- 고민 내용
- 문제 : 동일 유저와의 거래가 재성립될 시(동일 유저와 2개 이상의 상품 거래를 할 경우) 채팅방들의 메시지 내용이 동일, DB 안에 중복되는 정보(제품명, 판매자_id) 발생
- 원인 : 판매자와 구매자 정보로만 채팅방 생성
- 해결
- 기존 채팅방 생성 기준(판매자 + 구매자 정보) -> 판매자 + 구매자 + 상품(productId) 정보
- 서버 : 상품 DB에 낙찰자 정보 추가, 상품 DB기준으로 채팅방 생성
- 채팅방 정보에 상품명 추가
{users.map((val, idx) => {
return (
<Box
key={idx + 'msg'}
onClick={() => {
// 채팅방 이동
history.push(`/chat/${val._id}/${val.sellerunique === uid ? val.soldById : val.sellerunique}/${uid}/${val.sellerunique === uid ? val.soldBy : val.nickname}/${val.title}`);
}}
>
- 생략 -
</Box>
);
})}
-
정규표현식 문제
-
문제 : 아이폰 IOS, Safari 브라우저 흰 바탕의 빈 화면만 보여짐 -> syntaxError 발생
SyntaxError: Invalid regular expression: invalid group specifier name
-
해결 : Safari에서 지원하지 않는 특정 정규표현식(부등호) 사용이 문제 원인임을 발견
// 가격 콤마 정규식(input) export const input_priceComma = (price) => { return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",") };
-
-
CSS 호환 문제
- 문제 : CSS 일부가 Safari(+ios)에서 호환이 되지 않는 문제 발생 - flex gap, fit-content etc.
- 해결 : flex 대신 grid 사용
const ProductList = styled.div` display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; grid-column-gap: 38px; grid-row-gap: 50px; @media only screen and (max-width: 767px) { display: grid; grid-template-columns: 1fr; grid-row-gap: 20px; } `;