테스트 계정
PW:
123123
YEOUN (여행의 운치, 여운) : 전국 여행 정보를 모아볼 수 있는 여행 커뮤니티 서비스
- 각 지역의 여행 정보를 실시간으로 얻고 공유할 수 있습니다.
- 지도 기반으로 지역별 인기도를 한눈에 볼 수 있고, 지역별 관광지 정보도 확인할 수 있습니다.
- 기획 및 아이디어 구체화
- Figma 초기 화면 디자인
- GitHub 관리
- 작업 진행 상황 관리
- 버그 관리
- README 작성
- FE
- React / Styled-components / Axios
- ESLint / Prettier
- BE
- Node.js
- Express.js
- MongoDB
- JWT
- 버전 관리 : Git, GitHub
- 이슈 관리 : GitHub Issues
- 문서 관리 : Notion, Excel (API 명세서)
- 디자인 : Figma
- 메신저 : Discord
- API 테스트 : Postman
- FE: Netlify
- BE: GCP (+ Koyeb)
코드 컨벤션
- 들여쓰기(tab) 2칸 사용한다.
- JavaScript ES6+ 문법 사용한다.
- var 사용 금지한다.
- 세미콜론 필수로 사용한다.
- 작은 따옴표 사용한다.
- for, if문은 길이가 짧더라도 중괄호로 사용한다.
- 리액트 컴포넌트 확장자는 jsx로 통일한다.
- 함수 선언시에는 화살표 함수 사용한다.
네이밍 컨벤션
- 폴더명
- camelCase : 직접적으로 바로 React 컴포넌트가 들어있지 않은 간접적인 관계의 폴더
- PascalCase : React 컴포넌트가 바로 들어 있는 폴더
- 파일명
- React 컴포넌트 : PascalCase
- 이미지파일 : kebab-case
- jsx : PascalCase
커밋 컨벤션
- 커밋 메시지는 타입: 주제 (#이슈번호)와 같이 작성한다.
- 타입은 종류 중 하나만 선택하며, 영어 소문자로 시작한다.
- 타입 종류
- fix: 올바르지 않은 동작을 고친 경우
- feat: 새로운 기능을 추가한 경우
- refactor: 내부 로직은 변경하지 않고 코드를 개선한 경우
- style: 코드 개선과 상관없이 사소하게 코드를 수정한 경우
- design: 사용자 UI를 추가, 수정한 경우 (마크업, 퍼블리싱 작업)
- add: 폴더, 파일 등을 추가한 경우
- move: 폴더, 파일, 코드 등의 위치를 이동한 경우
- rename: 폴더명, 파일명 등을 수정한 경우
- remove: 폴더, 파일, 코드 등을 삭제한 경우
- assets: 에셋을 추가, 수정한 경우
- docs: 문서를 추가, 수정한 경우
- chore: 위의 모든 경우에 포함되지 않는 기타 수정사항
네이밍 컨벤션
- 폴더명
- 소문자로 시작하여 복수형으로 끝내기
- 파일명
- Mongoose Schema 파일 : 대문자로 시작
- 라우팅 파일 : 소문자로 시작하여 복수형으로 끝내기
- 유틸리티 함수 파일 : kebab-case
커밋 컨벤션
- 커밋 메시지는 '타입: 주제'와 같이 작성한다.
- 타입은 종류 중 하나만 선택하며, 영어 소문자로 시작한다.
- 타입 종류
- fix: 올바르지 않은 동작을 고친 경우
- feat: 새로운 기능을 추가한 경우
- refactor: 내부 로직은 변경하지 않고 코드를 개선한 경우
- style: 코드 개선과 상관없이 사소하게 코드를 수정한 경우
- add: 폴더, 파일 등을 추가한 경우
- docs: 문서를 추가, 수정한 경우
- chore: 위의 모든 경우에 포함되지 않는 기타 수정사항
main
├── develop
│ ├── feature/#1
│ │ │feature/#2
│___│___└── feature/#3
feature/이슈 번호
개인 작업 브랜치 생성, 기능 구현 후 develop에 PR
main
├── dev
│ ├── feat/기능
│ │ │fix/기능
│___│___└── refactor/기능
타입/기능
개인 작업 브랜치 생성, 기능 구현 후 dev에 PR
- Figma 디자인 제안 / 구체화 / 구현
- 프로젝트에 필요한 Asset 제작
- 헤더 / 푸터 / 레이아웃 / 지도 / 모달 / 버튼 / 캐러셀 / 로딩 스피너 컴포넌트 제작
- 메인 / 로그인 / 회원가입 / 마이 페이지 / 게시글(상세, 등록, 수정) / 프로필 설정 / 관광지 / 404 페이지 퍼블리싱
- 체크박스 커스텀
- 회원가입 / 로그인 / 로그아웃 기능 / 회원 정보 설정 / 회원 탈퇴
- 유효성 검증에 따른 버튼 활성화 여부 결정
- 로그인 성공 시, 토큰을 쿠키에 저장
- 프로필 이미지 리사이징을 통한 성능 향상
- 지도 기능
- 커뮤니티 내 지역별 인기도 확인
- 원하는 지역 클릭 시, 해당하는 지역의 관광지 페이지로 이동
- 게시물 필터링 / 검색 기능
- 최신 순, 인기 순, 댓글 순 필터링 (검색 키워드에도 적용)
- 지역별 필터링 (검색 키워드에도 적용)
- 로딩 스피너를 통한 UX 향상
- 게시물 무한 스크롤
- react-intersection-observer를 활용한 무한 스크롤 구현
- 게시물 등록 / 수정 / 삭제 기능
- 이미지 리사이징을 통해 성능 향상
- 지역 선택, 글 제목, 내용, 이미지(필수 아님)에 따른 (작성, 수정)버튼 상태관리
- 사용자가 이미지 등록하지 않을 경우 기본 이미지 설정
- 게시물 삭제 클릭 시, 모달 창으로 확인 요구
- 게시물 좋아요 / 댓글 / 스크랩 기능
- (본인의 댓글만 삭제 가능) 댓글 삭제 시, 모달 창으로 확인 요구
- 스크랩 클릭 시, 해당 유저의 카테고리 목록을 가져오며 추가 카테고리 생성 가능 (최대 10개)
- 동일한 게시물을 여러 카테고리에 스크랩 가능
- 스크랩 해제 시, 스낵바를 통해 UX 향상
- 마이 페이지
- 내가 쓴 글 확인 기능
- 내가 쓴 댓글 확인 및 삭제 기능
- 스크랩 목록 확인 기능
- 스크랩 카테고리명 수정 및 삭제 기능
- 관광지
- 페이지네이션을 통한 성능 향상
- 해당 관광지 클릭 시, 포털 사이트로 이동하여 추가 정보 획득 가능
- 회원 정보 조회 기능
- 각 페이지에 맞는 형태(카드 플립, 모달)로 유저의 프로필 이미지, 닉네임, 소개 확인 기능
- 탈퇴한 회원에 대한 처리 기능
- 탈퇴한 회원의 게시물은 확인 가능, 회원 정보(프로필 이미지, 소개)는 확인 불가능
- 탈퇴한 회원의 게시물에 좋아요, 스크랩 차단
- 인증(로그인) 여부에 따른 각 페이지 접근 제한
- SVG Sprite
- 배포(Netlify) 및 버그 관리
- 회원가입 / 로그인
- Crypto 패키지 사용해 비밀번호 암호화
- 회원 정보 유효성 검사를 통해 오류 처리
- 이메일, 닉네임 중복 확인
- 지역별 관광지 정보
- 오픈 API 활용해 데이터 가공
- 지역 기반 필터링
- 관광지명, 위치, 이미지 데이터 반환
- 시도명 정식 명칭과 매핑 작업 ex) 충북 -> 충청북도
- 관광지 정보 페이지네이션 구현
- 마이페이지
- 내가 쓴 글 조회
- 프로필 조회
- 회원 정보(이미지, 닉네임, 이메일, 소개) 변경
- 비밀번호 변경
- 회원 탈퇴
- 프로젝트 아이디어 제안
- Figma 초기 화면 디자인 / 구체화
-
로그인 / 로그아웃 / 회원 탈퇴 (토큰, 쿠키 활용)
- 로그인 시 토큰이 생성되어 쿠키에 저장됨
- 로그아웃, 회원 탈퇴 시 쿠키가 삭제됨
-
메인페이지
- 게시물 전체 목록 조회
- 로그인 시 게시물마다 좋아요, 스크랩 여부 보이게 처리
- lean으로 성능 개선
* 아래 4개의 기능 중복 적용 가능
- 무한스크롤을 위한 페이지네이션
- 지역별 필터링
- 최신순, 인기순, 댓글순 정렬
- 게시물 검색
-
게시물 조회 / 등록 / 수정 / 삭제 기능
- 기능마다 유효성 검사를 통해 오류 처리
- 게시물 삭제 시 관련 댓글, 좋아요, 스크랩 함께 삭제
-
게시물 좋아요 / 댓글 / 스크랩 기능
- 기능마다 유효성 검사를 통해 오류 처리
- 게시물마다 회원의 좋아요, 스크랩 여부 보이게 처리
- 댓글 작성시간 한국시간으로 변환
-
마이페이지
- 내가 쓴 댓글 확인 기능
- 내가 스크랩한 글 확인 기능
- lean으로 성능 개선
-
지도 기능
- 커뮤니티 내 지역별 인기도 확인
-
탈퇴한 회원에 대한 처리 기능
- 탈퇴한 회원의 게시물과 댓글은 확인 가능
- 탈퇴한 회원의 스크랩 삭제, 회원 정보 확인 불가능
-
인증(로그인) 여부에 따른 각 기능 제한
- Koyeb: 깃허브 기반 배포 -> 서버 성능 및 속도 저하 문제로 인해 GCP로 대체
- GCP (Google Cloud Platform): Compute Engine 사용 -> 도메인을 위해 최종적으로 App Engine 사용
- 개발 과정에서 편리함 고려 (ex. 상태코드 문자화)
- 파일 및 보안 관리 (ex. .gitignore, .env)
- 지도 기반 인기도 확인 기능
- 인기도는 각 지역별 게시물 수 + 좋아요 수 + 댓글 수로 산정
- 지역별 인기도 시각화를 위해 컬러를 보간하는 커스텀 훅 개발
/*useColorInterpolator.jsx 일부*/
const useColorInterpolator = (sortedPopularity) => {
const getColor = useCallback(
(score) => {
const minScore = ... // 인기가 가장 낮은 지역의 인기도 점수
const maxScore = ... // 인기가 가장 높은 지역의 인기도 점수
// 최소값과 최대값 사이에서 점수를 정규화
const normalizedScore = Math.min(Math.max(score, minScore), maxScore);
// 컬러 스케일 설정
const lightColor = [220, 230, 240];
const darkColor = [87, 127, 160];
// 정규화된 점수에 따라 색상을 계산
const interpolatedColor = lightColor.map((channel, index) => {
const minChannelValue = channel;
const maxChannelValue = darkColor[index];
const channelRange = maxChannelValue - minChannelValue;
// 정규화된 점수에 따라 색상 채널 값을 계산
const channelValue = minChannelValue + (normalizedScore / maxScore) * channelRange;
return Math.round(channelValue);
});
return `rgb(${interpolatedColor[0]}, ${interpolatedColor[1]}, ${interpolatedColor[2]})`;
},
[sortedPopularity],
);
return getColor;
};
export default useColorInterpolator;
- Spritebot 활용을 통해 sprite-sheet를 생성
- SVG sprite를 통해 이미지 용량 경량화 및 퍼포먼스 향상 (lighthouse 측정 결과 퍼포먼스 점수 10% 상승)
- 생성된 sprite-sheet를 컴포넌트화해서 사용
/*GlobalSVGSprite.jsx 일부*/
const spriteSheet = (
<svg xmlns='http://www.w3.org/2000/svg'>...</svg>
);
const GlobalSVGSprite = () => {
const GlobalSVG = document.querySelector('#GlobalSVG');
return createPortal(spriteSheet, GlobalSVG);
};
export default GlobalSVGSprite;
- createPortal을 활용해 지정된 id(GlobalSVG)에 해당 요소를 넣음
- 필요한 아이콘들이 하나의 파일로 제공되기 때문에 브라우저는 한 번 다운로드한 후 이를 캐시
- 때문에 불필요한 네트워크 요청이 줄어들며, 이로 인해 웹 페이지의 로딩 시간이 단축되어 사용자 경험이 향상
- aria-label 사용으로 접근성 보완
/*LocalSVGSprite.jsx 일부*/
const LocalSVGSprite = ({
id,
color,
width = '100%',
height = '100%',
ariaLabel,
onClickHandler,
$ref,
cursor = 'pointer',
}) => {
return (
<svg
fill={color}
width={width}
height={height}
aria-label={ariaLabel}
onClick={onClickHandler}
ref={$ref}
style={{ cursor: `${cursor}` }}
>
<use href={`#${id}`} />
</svg>
);
};
export default LocalSVGSprite;
- 비밀번호 암호화를 위해 해시 함수 사용
- sha1 알고리즘으로 해싱
const crypto = require('crypto');
module.exports = (password) => {
const hash = crypto.createHash('sha1');
hash.update(password);
return hash.digest("hex");
}
- 매번 오픈 API 호출할 경우 성능 저하 이슈 발생하므로, 필요한 데이터 파싱해서 리스트에 저장 후 사용
/*tour-info.js*/
var tourData = [];
request({
url: url + queryParams,
method: 'GET'
}, function (error, response, body) {
if(error) {
console.log(error)
}
var parsedJson = JSON.parse(response.body);
for(i in parsedJson['response']['body']['items']['item']) {
tourData.push({
name : parsedJson['response']['body']['items']['item'][i]['galTitle'], // 관광지명
location : parsedJson['response']['body']['items']['item'][i]['galPhotographyLocation'], // 관광지 위치
img : parsedJson['response']['body']['items']['item'][i]['galWebImageUrl'] // 관광지 이미지
})
}
});
module.exports = tourData;
- 지역명에 맞는 정보 필터링해서 반환
/*tours.js 일부*/
const filteredTours = tourInfo.filter((tour) => {
const location = JSON.stringify(tour.location).split(' ')[0];
return location.includes(region) || location.includes(mappedRegion);
}
);
- 하나의 API에서 로그인/로그아웃 시 서로 다른 동작이 필요함
- passport-jwt는 토큰 정보가 필수여서 새로운 패키지 도입
router.get('/', passport.authenticate(['jwt', 'anonymous'], { session: false }), asyncHandler(async (req, res) => {
const userId = req.user ? req.user._id : null;
// 메인페이지 로직 ...
});
- 로그인 상태에서는 해당 유저의 각 게시물마다 좋아요/스크랩 여부를 표시해야 함
- Promise.all()을 사용해 각 await이 끝날 때까지 기다리게 하여 알맞은 응답값을 리턴함
result = await Promise.all(
posts.map(async(post) => {
let likeState = false;
let scrap = false;
if (await Like.exists({user: userId, post: post})) {
likeState = true;
}
if (await Collection.exists({user: userId, posts: post})) {
scrap = true;
}
return {post, likeState, scrap};
})
);
회원가입 / 로그인 / 로그아웃 |
---|
프로필 설정 |
---|
게시글 작성 |
---|
게시글 수정 |
---|
게시글 삭제 |
---|
게시글 검색 |
---|
게시글 정렬 |
---|
게시글 필터링 |
---|
좋아요, 댓글 |
---|
프로필 확인 |
---|
관광지 페이지 |
---|
지도 |
---|
내가 쓴 글 / 내가 쓴 댓글 |
---|
스크랩 |
---|
스크랩 해제 / 카테고리 수정 및 삭제 |
---|
회원 탈퇴 |
---|
📂 yeoun-client
├─ 📂 public
│ ├─ ⭐ favicon.ico
│ └─ 📜 index.html
└─ 📂 src
├─ 📂 api
├─ 📂 assets
│ ├─ 📂 fonts
│ └─ 📂 images
├─ 📂 components
│ ├─ 📂 carousel
│ ├─ 📂 common
│ ├─ 📂 Loading
│ ├─ 📂 map
│ ├─ 📂 routes
│ ├─ 📂 ScrollToTop
│ └─ 📂 SVGSprite
├─ 📂 context
├─ 📂 hooks
├─ 📂 pages
│ ├─ 📂 homePage
│ ├─ 📂 LoginPage
│ ├─ 📂 myPage
│ ├─ 📂 NotFoundPage
│ ├─ 📂 postPage
│ ├─ 📂 ProfileSettingPage
│ ├─ 📂 signupPage
│ └─ 📂 touristAttractionPage
├─ 📂 styles
├─ 📄 App.jsx
└─ 📄 index.jsx
src/api/
: API 클라이언트 및 엔드포인트 URL을 정의src/assets/
: 서비스에서 사용하는 에셋 파일src/components/
: 재사용 가능한 컴포넌트src/context/
: 전역 데이터를 공유하기 위해 정의한 Context 파일src/hooks/
: 재사용 가능한 Custom Hooksrc/pages/
: 공통 컴포넌트를 사용해 만든 페이지src/styles/
: 전역 스타일 파일
📂 yeoun-server
├─ 📂 config
├─ 📂 models
├─ 📂 routes
│ └─ 📄 index.js
├─ 📂 utils
└─ 📄 app.js
config/
: 설정 관련 파일 (ex. 사용자 인증 위한 passport.js)models/
: Mongoose Schema 정의routes/
: 기능별 라우팅 및 비즈니스 로직utils/
: 유틸리티 함수 파일
김의호 | 배자현 | 서민경 | 조세영 |
---|---|---|---|
euihokim | bizbaeja | slz6k | ilu25 |
FE | FE | BE | BE |
- 공공데이터포털 (한국관광공사) : 오픈 API를 활용하여 관광지 정보 제공
- Lorem Picsum : 캐러셀 및 게시물 default 이미지로 사용
- Unsplash : 프로필 / 게시물 이미지 사용
- 지도 : 지도