Skip to content

Commit

Permalink
feat: 개개인 장소 입력 화면 구현 및 api 연동 코드 작성
Browse files Browse the repository at this point in the history
✨ feat: 개개인 장소 입력 화면 구현 및 api 연동 코드 작성
  • Loading branch information
Cllaude99 authored May 26, 2024
2 parents 69658e2 + c4ba7bd commit a1c8c52
Show file tree
Hide file tree
Showing 14 changed files with 676 additions and 35 deletions.
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@tanstack/react-query": "^5.29.2",
"axios": "^1.6.8",
"framer-motion": "^11.1.1",
"jotai": "^2.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.3",
Expand Down
18 changes: 18 additions & 0 deletions src/apis/enter-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios from 'axios';
import axiosInstance from '.';

export const fetchIsValidRoomId = async (roomId: string) => {
const { data } = await axios.get(`/api?roomId=${roomId}`);
return data;
};

export const fetchSavePlace = async (data: any) => {
return axiosInstance.post('/api/place-rooms', data, {
withCredentials: true,
});
};

export const fetchRoomUsersInfo = async (roomId: string) => {
const { data } = await axiosInstance.get(`/api/place-rooms/${roomId}/users`);
return data;
};
67 changes: 67 additions & 0 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// login이 완료된 사람의 요청의 경우 axiosInstance를 사용하여 요청한다

import axios from 'axios';

const REFRESH_URL = ''; // Refresh Token을 사용해 새로운 Access Token을 받을때 요청하는 URL

// access token 재발급하는 함수
const getNewToken = async () => {
try {
const accessToken = '';
const refreshToken = '';
return { accessToken, refreshToken };
// Refresh Token을 사용하여 REFRESH_URL로 요청을 하여 새로운 Access Token, Refresh Token을 받아와 2값을 리턴하도록 구현
} catch (e) {
// Refresh Token에 문제가 있는 상황이며 이는 올바르지 않은 유저 로그인 과정이므로, 로그아웃 처리
logout();
}
};

// 로그 아웃 함수
const logout = () => {
localStorage.removeItem('accessToken');
};

const axiosInstance = axios.create({
baseURL: '',
});

// 요청 인터셉터
axiosInstance.interceptors.request.use(
(config) => {
// 헤더에 엑세스 토큰 담기
const accessToken: string | null = localStorage.getItem('accessToken');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);

// 응답 인터셉터
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const { config, response } = error;
// 401에러가 아니거나 재요청이거나 refresh 요청인 경우 그냥 에러 발생
if (response.status !== 401 || config.sent || config.url === REFRESH_URL) {
return Promise.reject(error);
}
// 아닌 경우 토큰 갱신
config.sent = true; // 무한 재요청 방지
const newToken = await getNewToken();
if (newToken) {
localStorage.setItem('accessToken', newToken.accessToken);
localStorage.setItem('refreshToken', newToken.refreshToken);
config.headers.Authorization = `Bearer ${newToken.accessToken}`;
}
return axios(config); // 재요청
},
);

export default axiosInstance;
4 changes: 3 additions & 1 deletion src/components/button.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
interface IButton {
text: string;
isLoading: boolean;
onClick?: () => void;
}

export default function Button({ text, isLoading }: IButton) {
export default function Button({ text, isLoading, onClick }: IButton) {
return (
<button
type="submit"
className="min-h-10 primary-btn disabled:bg-neutral-400 disabled:text-neutral-300 disabled:cursor-not-allowed"
disabled={isLoading}
onClick={onClick}
>
{isLoading ? '잠시만 기다려 주세요...' : text}
</button>
Expand Down
3 changes: 3 additions & 0 deletions src/components/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Login() {
return <h1>로그인</h1>;
}
46 changes: 46 additions & 0 deletions src/pages/enter-location.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import SideBar from '@/components/Sidebar';
import { Outlet } from 'react-router-dom';
import styled from 'styled-components';

export default function EnterLocation() {
return (
<Container className="max-w-[1800px]">
<SideBar />
<Content>
<Title className="text-4xl font-medium pt-9">모두의 중간</Title>
<Textbox className="rounded-lg">
<p>상대방에게 링크를 공유하여 주소를 입력하게 하고 중간 지점을 찾아보세요!</p>
</Textbox>
<Outlet />
</Content>
</Container>
);
}

const Container = styled.div`
display: flex;
margin: 0 auto;
width: 100vw;
`;

const Content = styled.div`
width: 80%;
min-width: 1024px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
`;

const Title = styled.h1``;

const Textbox = styled.div`
width: 55%;
min-width: 700px;
background: rgba(81, 66, 255, 0.1);
color: ${(props) => props.theme.mainColor};
padding: 10px 5px;
font-size: 18px;
text-align: center;
`;
102 changes: 68 additions & 34 deletions src/pages/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ import SideBar from '@/components/Sidebar';

// 운행 수단
enum Transport {
Subway = '지하철',
Bus = '버스',
public = '지하철',
car = '자동차',
}

// 입력란의 형식
interface IFriendList {
readonly username: string;
readonly transport: Transport;
readonly address: string;
readonly siDo: string;
readonly siGunGu: string;
readonly roadNameAddress: string;
readonly addressLat: number;
readonly addressLong: number;
}

// 입력 폼으로 부터 받은 값의 형식
Expand All @@ -26,11 +30,19 @@ interface IForm {
}

// 입력란의 기본 형태 템플릿
const default_format: IFriendList = { username: '', transport: Transport.Subway, address: '' };
const default_format: IFriendList = {
username: '',
transport: Transport.public,
siDo: '',
siGunGu: '',
roadNameAddress: '',
addressLat: 0,
addressLong: 0,
};

export default function Home() {
const [isLoading, setIsLoading] = useState(false);
const [addresses, setAddresses] = useState<string[]>([]); // 사용자들의 주소 목록
const [isLoading, setIsLoading] = useState(false); // form제출 상태
const [addresses, setAddresses] = useState<string[]>([]); // 사용자들의 도로명 주소 목록
const { control, register, handleSubmit, setValue, watch } = useForm<IForm>({
defaultValues: {
friendList: [default_format],
Expand All @@ -46,64 +58,83 @@ export default function Home() {
new window.daum.Postcode({
oncomplete: function (data: any) {
const fullAddress = data.roadAddress;
setValue(`friendList.${index}.address`, fullAddress);
setAddresses((prev) => {
const newAddresses = [...prev];
newAddresses[index] = fullAddress;
return newAddresses;
const siDo = data.sido;
const siGunGu = data.sigungu;
const roadNameAddress = data.roadAddress;

// Kakao 지도 API를 사용하여 위도와 경도 가져오기
const geocoder = new window.kakao.maps.services.Geocoder();
geocoder.addressSearch(fullAddress, function (result: any, status: any) {
if (status === window.kakao.maps.services.Status.OK) {
const addressLat = parseFloat(result[0].y);
const addressLong = parseFloat(result[0].x);

// 주소와 좌표 설정
setValue(`friendList.${index}.siDo`, siDo);
setValue(`friendList.${index}.siGunGu`, siGunGu);
setValue(`friendList.${index}.roadNameAddress`, roadNameAddress);
setValue(`friendList.${index}.addressLat`, addressLat);
setValue(`friendList.${index}.addressLong`, addressLong);

setAddresses((prev) => {
const newAddresses = [...prev];
newAddresses[index] = fullAddress;
return newAddresses;
});
}
});
},
}).open();
};

// API 요청을 위한 mutation 정의
// 중간지점찾기 API 요청
const { mutate: searchMiddlePoint } = useMutation({
mutationFn: (data: Omit<IFriendList, 'username'>[]) => {
// + 추후에 요청 url의 앞부분에 백엔드의 url을 붙여주어야 한다. (백엔드의 url은 .env파일에 넣고 관리할 것)
mutationFn: (data: any) => {
return axios.post('/api/middle-points', data);
},
onSuccess: (data, variable) => {
// 요청 성공시 응답으로 부터 얻은 중간 지점 장소 리스트를 중간 지점 화면에 navigate의 state로 보내준다.
// navigate('중간 지점 화면을 보여줄 주소', {state: data});
console.log('API 요청 성공');
console.log('중간지점찾기 API 요청시 보낸 데이터', variable);
console.log('중간지점찾기 API 요청 이후 받은 응답 데이터', data);
},
onError: (error) => {
// 에러 발생시 alert로 에러를 보여준다. (서버 에러)
console.error(`중간 지점 결과 조회 API 요청 실패, 에러명 : ${error}`);
setIsLoading(false);
},
});

// 중간 지점 찾기 클릭시 수행되는 함수
// 중간지점찾기 버튼 클릭시 수행되는 함수
const onSubmit = (data: IForm) => {
// form 보내는 상태를 true로 설정해준다
setIsLoading(true);
const allFieldsFilled = data.friendList.every(
(friend) =>
friend.username &&
friend.transport &&
friend.roadNameAddress &&
friend.siDo &&
friend.siGunGu &&
friend.addressLat &&
friend.addressLong,
);

// friendList에 있는 모든 사람에 대해 이름, 운송수단, 주소(위치)가 모두 입력되었는지 확인하는 과정
const allFieldsFilled = data.friendList.every((friend) => friend.username && friend.transport && friend.address);

// 모두 입력되지 않은 경우에는 '모두 입력해주세요' 라는 에러메세지 보여주고 종료한다.
if (!allFieldsFilled) {
setIsLoading(false);
alert('모둗 입력해주세요!');
alert('모두 입력해주세요!');
return;
}

// username을 제외한 데이터 추출
const submissionData = data.friendList.map(({ username, ...rest }) => rest);
const submissionData = data.friendList.map(({ username, transport, ...rest }) => ({
...rest,
transport: transport === Transport.public ? 'public' : 'car',
}));

console.log('중간지점 찾기 요청시 서버로 보내는 값', submissionData);

// API 요청 보내기
searchMiddlePoint(submissionData, {
onSuccess: () => {
console.log('2번째로 불림- API 요청 성공');
},
onError: (error) => {
// 에러 발생시 에러를 보여준다. (서버 에러)
console.error(`2번째로 불림중간 지점 결과 조회 API 요청 실패, 에러명 : ${error}`);
console.error(`2번째로 불림 중간 지점 결과 조회 API 요청 실패, 에러명 : ${error}`);
setIsLoading(false);
},
});
Expand Down Expand Up @@ -152,12 +183,15 @@ export default function Home() {
</div>
<div className="relative overflow-x-scroll w-72 hide-scrollbar hide-x-scrollbar">
<div
className={`flex items-center min-w-full h-10 px-3 transition bg-indigo-100 w-max border-none rounded-lg cursor-pointer ring-1 focus:ring-2 ring-indigo-100 ${watch(`friendList.${index}.address`) ? 'text-black' : 'text-gray-500'}`}
className={`flex items-center min-w-full h-10 px-3 transition bg-indigo-100 w-max border-none rounded-lg cursor-pointer ring-1 focus:ring-2 ring-indigo-100 ${watch(`friendList.${index}.roadNameAddress`) ? 'text-black' : 'text-gray-500'}`}
onClick={() => openAddressSearch(index)}
>
{watch(`friendList.${index}.address`) || '주소 입력'}
{watch(`friendList.${index}.roadNameAddress`) || '주소 입력'}
</div>
<input type="hidden" {...register(`friendList.${index}.address` as const, { required: true })} />
<input
type="hidden"
{...register(`friendList.${index}.roadNameAddress` as const, { required: true })}
/>
</div>
</div>
</div>
Expand All @@ -170,7 +204,7 @@ export default function Home() {
>
+
</button>
<Button isLoading={isLoading} text="중간 지점 찾기" />
<Button isLoading={isLoading} text="중간 지점 찾기" onClick={handleSubmit(onSubmit)} />
</div>
<div className="w-[36%] rounded-xl h-[500px] -mt-8 shadow-lg">
<KakaoMap addresses={addresses} />
Expand Down
3 changes: 3 additions & 0 deletions src/pages/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Loading() {
return <h1>로딩중...</h1>;
}
Loading

0 comments on commit a1c8c52

Please sign in to comment.