일반회원
Test ID : user123@user.com
Test PW : user123!
판매회원
Test ID : seller123@seller.com
Test PW : seller123!
'마이파킹(MyParking)'은 주차장이 필요한 사람과 주차장을 판매하고 싶은 사람들 간을 이어주는 주차장 공유/대여 서비스입니다.
- 전국 어디서든 원하는 날짜와 위치를 찾아보고 주차장을 예약할 수 있습니다.
- 위치와 날짜 검색을 통해 내가 필요한 주차장을 파악할 수 있습니다.
- 장시간 사용하지 않는 주차장을 등록해서 부가 수익을 창출할 수 있습니다.
- (추후) 예약자와 판매자 간의 소통을 통해 원활하게 주차장을 예약할 수 있습니다.
강수암 | 이현걸 | 이수아 |
---|---|---|
- 개발환경 세팅 및 라우터 설정 - Product 개발 - Map 기능 |
- Search, MyPage, Signup 개발 - Zustand Slice Pattern초기 세팅 - Map 기능 |
- purchase,reply,Login 개발 - MUI 적용, 다크모드 개발 |
🎨 UI
-
상품 등록, 수정 페이지
-
상품 리스트, 상세 페이지
-
홈 페이지
-
랜딩 및 에러페이지
�💡 기능 구현
-
상품 CRUD
-
카카오 지도 API
- 좌표 받아오고 위치 등록하기
- 현재 위치 받아오기
- 오버레이 적용 후 게시글 렌더링
🤝 기타
- 프로젝트 아이디어 기획 및 도출
- 지도 api 자료조사
- axios 인스턴스 설정
- 초기 개발환경 세팅
- 빌드 및 배포
🎨 UI
- 회원가입 페이지
- 마이 페이지
�💡 기능 구현
- 회원 가입 및 유효성 검사, 토큰 관리
- 마이 프로필 등록 및 수정
- 카카오 지도 API
- 키워드 및 날짜 검색 구현
🤝 기타
-
데일리 스크럼, 회의록 작성 및 관리
-
전역 상태관리 설정
-
지도 api 코드 최적화
🎨 UI
-
로그인 페이지
-
주문 목록 및 상세 페이지
-
결제 페이지
-
후기 페이지
-
레이아웃(헤더 푸터)
-
반응형 설정
�💡 기능 구현
-
로그인 유효성 검사 및 토큰 전역 관리
-
주문 목록 조회 및 상세 조회
-
후기 작성
-
결제 적용
🤝 기타
- 프로젝트 디자인 관리(피그마)
- MUI 초기 설정 및 적용
- 다크모드 지정
🚙 my-parking-app
├─ public
├─ src
│ ├─ components : 재사용을 위해 기능이 담긴 컴포넌트
│ │ ├─ UI : 공통으로 사용되는 UI 컴포넌트
│ │ ├─ common : 특정 도메인이 아닌 컴포넌트
│ │ │ └─ map : 지도 관련 컴포넌트
│ │ ├─ domain
│ │ │ ├─ auth : 인증, 유저 관리 컴포넌트
│ │ │ ├─ my-services : 회원기능 관련 컴포넌트
│ │ │ ├─ order-history : 주문기능 관련 컴포넌트
│ │ │ ├─ product: 상품기능 관련 컴포넌트
│ │ │ ├─ purchase : 구매기능 관련 컴포넌트
│ │ │ └─ reply : 후기기능 관련 컴포넌트
│ │ └─ layouts: 레이아웃 컴포넌트
│ ├─ pages : 라우팅 처리를 위한 페이지 컴포넌트
│ ├─ router : 최상위 라우터 관리 폴더
│ ├─ services : 공통 API 로직 폴더
│ ├─ store : zustand 전역 상태관리 폴더
│ ├─ types : 타입 정의 폴더
│ ├─ assets : 정적파일 폴더
│ ├─ App.module.css
│ ├─ App.tsx
│ ├─ index.css
│ ├─ main.tsx
│ └─ vite-env.d.ts
├─ tsconfig.json
├─ tsconfig.node.json
└─ vite.config.ts
로그인 및 회원가입
메인 서비스
상품 등록
상품 상세조회 및 수정 삭제
결제 및 주문기록 후기
Axios를 사용하여 API 요청을 처리했으며, axios 인스턴스를 적용하여 모든 axios 요청에 대한 공통 config를 적용하고 인터셉트를 추가하여 요청하고 응답받은 작업에 대한 전처리 및 후처리를 진행했습니다. 대표적으로 토큰 유효기간이 만료되었을 시, 리프래쉬 토큰을 가지고 다시 토큰을 발급받는 로직을 추가하였습니다.
import axios from "axios";
import mem from "mem";
import { BASE_URL } from "./BaseUrl";
// 리플래쉬 토큰 앤드포인트 경로
const REFRESH_URL = "/users/refresh"; // 리프래쉬 토큰을 서버에 보내는 주소 -> 서버에서 새로운 엑세스 토큰 보내줌
...
const instance = axios.create({
baseURL: BASE_URL,
timeout: 1000 * 5,
headers: {
"Content-Type": "application/json",
accept: "application/json",
},
withCredentials: true,
});
// 요청 인터셉트
instance.interceptors.request.use(
(config) => {
// 요청 전 토큰이 유효한지 리프래쉬토큰 검증 절차 진행
// console.log(config.url);
let token = userAccToken;
if (config.url === REFRESH_URL) {
// 요청을 보내는 객체 안에 리프래쉬토큰을 전달할 api주소가 있다면, 기존 받아온 리프래쉬 토큰 저장
token = userRefToken;
}
config.headers["Authorization"] = `Bearer ${token}`;
return config;
},
(error) => {
console.error("interceptors error", error);
return Promise.reject(error);
}
);
// 응답 인터셉터
instance.interceptors.response.use(
(response) => {
// console.log(response);
return response;
},
async (error) => {
console.error("interceptors error", error);
const { config, response } = error;
if (response?.status === 401) {
// 응답 에러 메시지가 TokenExpiredError이거나 요청에 리프래쉬토큰 보내는 주소api가 아니면 재발급을 진행합니다.
if (
response.data.errorName === "TokenExpiredError" &&
config.url !== REFRESH_URL
) {
console.log("accessToken 만료. 재발급이 필요합니다.");
const accessToken = await getAccessToken(instance);
if (accessToken) {
// 새롭게 전달받은 엑세스토큰을 에러에서 나온 config 헤더 오더라이제이션에 다시 담아줍니다.
config.headers.Authorization = `Bearer ${accessToken}`;
// 주스탄드user 상태값도 업데이트 해줍니다.
setUser(
{
accessToken: accessToken,
refreshToken: userRefToken,
},
user
);
// 기존 axios 설정값을 가지고 재요청
return axios(error.config);
}
} else {
// 리플래쉬 토큰마저 만료될경우에
alert("다시 로그인이 필요합니다.");
logout();
navigate("/login");
}
} else if (response?.status === 404) {
// 404오류뜨면 에러페이지로 이동시키기
navigate("/error");
} else {
const error = response?.data?.error;
// Network error 같은 경우 response가 없거나 서버에서 error를 응답한 경우
if (!response || error) {
alert(
error?.message ||
`요청하신 작업처리에 실패했습니다. 잠시후 다시 요청하시기 바랍니다.`
);
} else {
return Promise.reject(error);
}
}
}
);
// AccessToken 재발급
const getAccessToken = mem(
async function (instance) {
try {
const {
data: { accessToken },
} = await instance.get(REFRESH_URL);
return accessToken;
} catch (error) {
console.error(error);
alert("다시 로그인이 필요합니다.");
}
},
{ maxAge: 1000 }
);
return instance;
저희 서비스의 핵심적 기능을 구현하고자 카카오에서 제공하는 지도 라이브러리의 함수와 메서드를 적용했고, SDK에서 제공하는 컴포넌트를 필요에 따라 적용시켰습니다. 사용자가 원하는 위치의 주차장을 검색하거나 지도를 이동할 수 있고, 해당 지도 범위에 등록된 주차장이 마커를 통해 표시되도록 세팅하였습니다. 추가적으로 자신의 위치를 확인하고 지도를 이동할 수 있는 기능 또한 제공됩니다.
//상품 검색에 필요한 정보(searchInfo)를 변경하고, 사용자의 검색 의도에 따라 조건적으로 지도를 움직이는 함수
const handleSearch = () => {
...
//검색어 입력값이 있는 경우, 지도를 이동한 후 상품 검색
const ps = new kakao.maps.services.Places(map);
ps.keywordSearch(`${searchRef.current.value}`, placeSearchCB);
function placeSearchCB(
result: kakao.maps.services.PlacesSearchResult,
status: kakao.maps.services.Status
) {
if (!map) return;
if (status === kakao.maps.services.Status.OK) {
const data = result[0]; // 가장 유사한 상위검색객체 저장
//지도의 중심 좌표 이동
map.setCenter(new kakao.maps.LatLng(Number(data.y), Number(data.x)));
//지정한 영역이 가장 잘 보이는 최적의 지도 중심 좌표와 레벨이 지정
// const bound = new kakao.maps.LatLngBounds(); // 지도 영역생성 -> 사각형
// bound.extend(new kakao.maps.LatLng(Number(data.y), Number(data.x)));
// map.setBounds(bound);
// (추가)검색한 키워드, 중심좌표, 영역을 담은 객체 상태를 변경해줍니다.
setSearchInfo({
...searchInfo,
place_name: data.place_name,
period: period,
centerLatLng: {
lat: Number(data.y),
lng: Number(data.x),
},
});
}
}
};
useEffect(() => {
// 검색어에 해당하는 주차장 쿼리 요청 함수
const searchProducts = async () => {
if (!map) return;
const bound = map.getBounds();
const res = await searchItemsInThisBound(bound, searchInfo.period);
setMarkers(res); // 마커변경출력
setProducts(res); // 리스트변경출력
};
searchProducts();
}, [mapExist, searchInfo]);
...
<Map
center={{
lat: 37.5070100333146,
lng: 127.055618149788,
}}
style={{ height: "100vh" }}
level={level}
onCreate={(map) => {
setMap(map); // 생성
setMapExist(true);
}}
onZoomChanged={(map) => {
searchProducts();
setLevel(map.getLevel());
}}
onDragEnd={() => searchProducts()}
maxLevel={7}
>
...
- 일시: 평일 오전 9시 45분(15분 내외)
- 내용: 어제 한일 및 오늘 해야할 일 체크
- 대화방식: 정보 전달만이 아닌, 코드의 변화를 통해 기대되는 효과에 대해 토론 / 문제상황 공유 및 해결책 탐구
- 일시: 평일 오전 4시 30분(1시간 30분)
- 내용: 서로 작성한 코드에 대해 질문하고 고민해 보면서 수정
- 대화방식: 각자 구현했던 기능 로직에 대해 공유하고, 이해되지 않은 서로의 코드에 대해 서로 협력해서 이해하려 노력하는 시간으로도 사용