배포 URL | 테스트 ID | 테스트 PW |
---|---|---|
https://happy4.netlify.app/ |
[email protected] |
happypawpw |
- 🐱 반려동물의 정보를 공유하는 커뮤니티를 구축합니다.
- 🛍️ 원하는 물품을 판매하거나 구매하는 서비스를 제공합니다.
- 🧤 사용자들 끼리 팔로우 기능을 통해 서로의 소식을 공유할 수 있습니다.
- 🖼️ 글과 사진을 함께 업로드하여 반려 동물들의 일상을 자랑하고 공유할 수 있습니다.
- 🧡 다른 사용자들의 글에 좋아요를 누르고 댓글을 작성할 수 있습니다.
- 🗺️ 지도기능을 통해 내주변의 반려동물 카페, 병원, 호텔을 찾을 수 있습니다.
- 프로젝트 기획 및 개발 : 2023.06.01 ~ 2023.06.30
- 프로젝트 노션 :
해피포 노션 바로가기
Front-End | Back-End | Co-work & etc |
---|---|---|
제공된 API 사용
|
🐾 HappyPaw
├─ 📦 public
│ ├─ ⭐ favicon.ico
│ └─ 📃 index.html
├─ 📦 src
│ ├─ 📂 api
│ ├─ 📂 assets
│ │ ├─ 📂 icon
│ │ └─ 📂 splash
│ ├─ 📂 components
│ │ ├─ 📂 common
│ │ │ ├─ 📂 Button
│ │ │ ├─ 📂 Carousel
│ │ │ ├─ 📂 Header
│ │ │ ├─ 📂 Input
│ │ │ ├─ 📂 MainLayout
│ │ │ ├─ 📂 Modal
│ │ │ ├─ 📂 TabMenu
│ │ │ └─ 📂 Wrapper
│ │ ├─ 📂 Follow
│ │ ├─ 📂 Post
│ │ ├─ 📂 Product
│ │ ├─ 📂 Profile
│ │ └─ 📂 Skeleton
│ ├─ 📂 context
│ ├─ 📂 hooks
│ ├─ 📂 pages
│ │ ├─ 📂 ChatPage
│ │ ├─ 📂 ErrorPage
│ │ ├─ 📂 FollowListPage
│ │ ├─ 📂 HomePage
│ │ ├─ 📂 JoinPage
│ │ ├─ 📂 LoginPage
│ │ ├─ 📂 MapPage
│ │ ├─ 📂 PostPage
│ │ ├─ 📂 ProductPage
│ │ ├─ 📂 ProfilePage
│ │ ├─ 📂 SearchPage
│ │ └─ 📂 SplashPage
│ ├─ 📂 routes
│ ├─ 📂 styles
| ├─ 📜 App.js
| └─ 📜 index.js
- 타입은 종류 중 하나만 선택하며, 영어 소문자로 시작한다.
- 주제는 한글로 간단명료하게 작성한다.
- 주제의 마지막 문자로
.(마침표)
를 사용하지 않는다. - 주제는 '-다', '-음'과 같은 어미로 끝내지 않고, 과거형을 사용하지 않는다.
- 올바르지 않은 예 )
feat: 카카오 로그인 연동 기능을 추가했다, 혹은 추가함 (#3)
- 옳은 예 )
feat: 카카오 로그인 연동 기능 추가 (#3)
- 올바르지 않은 예 )
- 주제는 소스 코드를 보지 않고도 변경 사항이 무엇인지 알 수 있도록 작성해야 한다.
- 올바르지 않은 예 )
design: CSS 조정 (#4)
- 옳은 예 )
design: text box와 layout frame 사이에 left padding 추가 (#4)
- 올바르지 않은 예 )
- 커밋 메시지는 제삼자가 봤을 때 무엇을 했는지 파악할 수 있게 자세히 작성한다.
- 커밋 메시지는 어떻게 보단 무엇과 왜를 설명한다.
- 한 커밋에는 한 가지 기능만 담는다.
- 예 ) 화면 개발의 경우 : 컴포넌트 단위로 커밋
- 예2 ) 기능 개발의 경우 : 각 기능 단위로 커밋
- fix: 올바르지 않은 동작을 고친 경우
- feat: 새로운 기능을 추가한 경우
- refactor: 내부 로직은 변경하지 않고 코드를 개선한 경우
- style: 코드 개선과 상관없이 사소하게 코드를 수정한 경우
- design: 사용자 UI를 추가, 수정한 경우 (마크업, 퍼블리싱 작업)
- add: 폴더, 파일 등을 추가한 경우
- move: 폴더, 파일, 코드 등의 위치를 이동한 경우
- rename: 폴더명, 파일명 등을 수정한 경우
- remove: 폴더, 파일, 코드 등을 삭제한 경우
- assets: 에셋을 추가, 수정한 경우
- docs: 문서를 추가, 수정한 경우
- chore: 위의 모든 경우에 포함되지 않는 기타 수정사항
const colors = {
primary: '#374259',
secondary: '#b1b5bb',
third: '#F2D8D8',
gray: '#dbdbdb',
bgGray: '#f2f2f2',
txtColor: '#767676',
warning: '#FD7A6E',
white: '#fff',
};
const theme = { colors };
export default theme;
정현빈 | 박지윤 | 이상용 | 김미정 |
---|---|---|---|
팀장 | 팀원 | 팀원 | 팀원 |
hyeonbinnn | JiyunPark1301 | yongisadragon | goyomi |
스플래시 | 로그인 | 회원가입 & 프로필 설정 |
---|---|---|
홈 | 계정 검색 | 팔로우 & 팔로잉 |
---|---|---|
게시글 업로드 | 게시글 수정 | 게시글 삭제 |
---|---|---|
댓글 작성 | 댓글 삭제 | 댓글 신고 |
---|---|---|
상품 등록 | 상품 수정 | 상품 삭제 |
---|---|---|
프로필 수정 | 404 | 지도 |
---|---|---|
useEffect(() => {
if (!['email', 'accountname'].includes(id)) return;
if (
(id === 'email' && !EMAIL_REGEX.test(formData.email)) ||
(id === 'accountname' && !ID_REGEX.test(formData.accountname)) ||
formData['accountname'] === userAccountname
// 프로필 수정 페이지에서 현재 로그인한 유저의 accountname인 경우 이미 가입된 계정이라는 에러 메세지를 보여주지 않기 위함
) {
return;
}
const errorMsg = id === 'email' ? '이미 가입된 이메일 주소 입니다.' : '이미 가입된 계정ID 입니다.';
const timer = setTimeout(() => {
checkDuplication(errorMsg);
}, 300);
return () => {
clearTimeout(timer);
};
}, [formData.email, formData.accountname]);
-
email
과accountname
만 중복 검사를 진행하기 때문에id
가 다른 값이 되면return
을 합니다.
그 다음formData.email
,formData.accountname
이 변경될 때마다 실행이 됩니다. -
조건문을 통해 입력된 이메일과 계정 ID의 형식이 올바른지 확인한 뒤,
formData.accountname
이 현재 로그인한 사용자의 계정 ID와 일치하지 않는지 확인 후, 만약 조건에 해당하지 않는다면 함수를 종료합니다. -
그 이외에, 중복된 이메일 또는 계정 ID 에러 메시지를 설정하고, 300밀리초 후에
checkDuplication
함수를 호출합니다.
timer
변수에는setTimeout
함수로 생성된 타이머 ID가 저장되며,clearTimeout
을 사용하여 타이머 취소가 가능합니다. -
useEffect
의 반환 함수는 해당 이펙트가 정리(clean-up)될 때 실행하고, 여기서 타이머를 취소하기 위해clearTimeout
을 호출합니다.
디바운싱 기능을 적용함으로써 사용자가 입력할 때마다 서버요청을 하지 않기에 통신 비용이 발생하지 않습니다.
import { useState } from 'react';
import { uploadImages } from '../api/image';
const ALLOWED_EXTENSIONS = ['.jpg', '.gif', '.png', '.jpeg', '.bmp', '.tif', '.heic'];
const MAX_SIZE = 10 * 1024 * 1024;
const useImagesUpload = () => {
const [images, setImages] = useState([]);
const onUpload = async (files, length) => {
if (images.length + length > 3) return alert('이미지는 최대 3개까지 업로드 할 수 있습니다.');
const formData = new FormData();
for (let i = 0; i < length; i++) {
const file = files[i];
const fileExtension = file.name.split('.').pop().toLowerCase();
if (ALLOWED_EXTENSIONS.includes(`.${fileExtension}`) && file.size <= MAX_SIZE) {
formData.append('image', file);
}
}
try {
const data = await uploadImages(formData);
const filenames = data.map((data) => data.filename);
setImages((prev) => [...prev, ...filenames]);
} catch (error) {
console.log(error.message);
}
};
const onDelete = (index) => {
setImages((prevImages) => {
const updatedImages = [...prevImages];
updatedImages.splice(index, 1);
return updatedImages;
});
alert('삭제되었습니다.');
};
return { images, onUpload, onDelete };
};
export default useImagesUpload;
-
이미지 업로드는 특성상 회원가입 시 프로필 설정, 프로필 수정, 게시글 작성, 상품 등록 등 여러 페이지에서 반복적으로 사용됩니다. 그래서 여러 이미지를 업로드 할 수 있는 상태를 선언하고, 새롭게 들어오는 이미지들과 기존 이미지를 합친 값이 3이 넘으면 더 이상 업로드 할 수 없도록 구현합니다.
-
FormData
객체를 생성하고,files
배열을 순회하면서 허용되는 확장자 목록과 이미지 사이즈를 검사한 뒤, 통과한다면formData
에image
라는 키 값으로 파일을 추가할 수 있습니다. -
그 다음
uploadImages
함수를 통해 서버에 이미지를 업로드하고, 서버 응답에서 파일명을 추출하여 상태를 업데이트하고,setImages
함수를 사용해 이전 상태값인prev
배열과 새로운 파일명 배열인filenames
를 합쳐서 상태를 갱신합니다. -
이렇게 함으로써, 이미지 업로드 후의 상태값을 업데이트하고
React
컴포넌트가 리렌더링될 수 있도록 합니다.
const BASE_URL = 'https://api.mandarin.weniv.co.kr';
export const request = async (url, options) => {
try {
const response = await fetch(`${BASE_URL}/${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (response.ok) {
const data = await response.json();
return data;
}
} catch (err) {
console.log(err);
}
};
export const imageRequest = async (url, options) => {
try {
const response = await fetch(`${BASE_URL}/${url}`, { ...options });
if (response.ok) {
const data = await response.json();
return data;
}
} catch (err) {
console.log(err);
}
};
- 가장 기본이 되는
request
함수를 만들어 줍니다. 그 다음, 로그인은auth.js
, 게시글은post.js
, 상품은product.js
등 기능별로 파일을 만듭니다.
import { request } from './request';
// 회원가입
export const join = async (state, formData, image) => {
return await request('user', {
method: 'POST',
body: JSON.stringify({ user: { ...state, ...formData, image } }),
});
};
// 로그인
export const login = async (email, password) => {
return await request('user/login', {
method: 'POST',
body: JSON.stringify({ user: { email, password } }),
});
};
// 토큰 검증
// ...
// 이미 존재하는 이메일(또는 계정)인지 검사
export const validateForm = async (id, formData) => {
return await request(`user/${id}valid`, {
method: 'POST',
body: JSON.stringify({ user: { [id]: formData[id] } }),
});
};
-
파일 내에서 필요한 요청들 즉,
auth.js
는 회원가입, 로그인 등post.js
는 게시글 불러오기, 업로드, 수정, 삭제, 신고 등 각각의 함수로 만들어 줍니다. -
그리고 각각의 함수를 사용할 때는 먼저 import 해주고, api를 가져온 후 필요한 인자 값들을 넘겨주면 됩니다.