프론트 공용 문서 - To-Letter/To-Letter-front GitHub Wiki
public
├── images(이미지 파일 관리 폴더)
│ ├── scenery(풍경 구현에 필요한 이미지 관리 폴더)
├── models(glb파일 관리 폴더)
src
├── api(api 관련 로직들을 정리)
│ ├── controller(서버와의 통신에 대한 엔드포인트 및 파라미터 관리 폴더)
├── components(재사용 컴포넌트들을 담은 폴더)
│ ├── account(사용자 계정에 대한 컴포넌트 폴더)
│ ├── Room(방안에서 사용되는 사물 컴포넌트 폴더)
│ ├── Scenery(풍경 관련 컴포넌트 폴더)
├── contants(풍경 관련 상수값 관리 폴더)
├── context(context 코드 폴더)
├── pages(페이지 관리 폴더)
├── recoil(recoil관련 폴더)
├── utils(공통적으로 사용되는 헬퍼 함수나 유틸리티 기능)
- main color :
#cbb1a0
- sub color :
#7b5d54
ToastMessage 타입 정의
interface ToastMessageProps {
message: string;
duration?: number; // 메시지가 표시되는 시간 (기본값: 3초)
onClose: () => void;
}
useState를 이용해 관리
const [toast, setToast] = useState<{ message: string; visible: boolean }>({
message: "",
visible: false,
});
setter로 message와 visible의 유무를 업데이트
setToast({
message: "회원가입 성공!",
visible: true,
});
toast message를 사용하고자 하는 구간에 추가하여 사용
{toast.visible && (
<ToastMessage
message={toast.message}
onClose={() => setToast({ ...toast, visible: false })}
/>
)}
PopupContext 타입 정의
createContext를 이용하여 PopupContext생성
interface PopupContextProps {
activePopup: string | null;
setActivePopup: (popupId: string | null) => void;
}
const PopupContext = createContext<PopupContextProps | undefined>(undefined);
Provider를 이용해 하위 컴포넌트에세 값 제공
export const PopupProvider = ({ children }: { children: ReactNode }) => {
const [activePopup, setActivePopup] = useState<string | null>(null);
return (
<PopupContext.Provider value={{ activePopup, setActivePopup }}>
{children}
</PopupContext.Provider>
);
};
usePopup을 이용해 context값을 전역적으로 관리
export const usePopup = () => {
const context = useContext(PopupContext);
if (!context) {
throw new Error("usePopup must be used within a PopupProvider");
}
return context;
};
import { atom } from 'recoil';
// 로딩 상태를 관리하는 Recoil atom
export const loadingState = atom<boolean>({
key: 'loadingState', // 고유 키
default: false, // 초기값 false
});
...
import { loadingState } from '../recoil/loadingAtom';
...
const LoadingSpinner: React.FC = () => {
const isLoading = useRecoilValue(loadingState); // Recoil에서 로딩 상태 가져오기
if (!isLoading) return null; // 로딩 중이 아닐 때는 컴포넌트를 렌더링하지 않음
return (
<SpinnerOverlay>
<Spinner />
</SpinnerOverlay>
);
};
export default LoadingSpinner;
const MailVerify: React.FC = () => {
.
.
const setLoding = useSetRecoilState(loadingState)
.
.
// 이메일 인증 요청
const submitSignup = async () => {
if (!verifyMe) {
setToast({ message: "인증 요청 버튼을 먼저 눌러주세요.", visible: true });
} else if (mailKey === "" || mailKey.length !== 6) {
setToast({ message: "인증 키가 제대로 입력되지 않았습니다.", visible: true });
} else {
try {
setLoding(true);
let res: any = await postEmailVerify({
email: email,
randomCode: mailKey,
});
if (res.data.responseCode === 200) {
setLoding(false);
setToast({ message: "회원가입 성공!", visible: true });
setModalState({
isOpen: true,
type: "login",
});
} else if (res.data.responseCode === 401) {
setLoding(false);
setAuthReqMessage(true);
setVerifyMe(true);
setToast({ message: "시간 초과로 인증코드를 다시 보냈습니다.", visible: true });
} else if (res.data.responseCode === 403) {
setLoding(false);
setToast({ message: "인증 코드가 불일치합니다.", visible: true });
} else if (res.data.responseCode === 404) {
setLoding(false);
setToast({ message: "메일이 존재하지 않습니다. 다른 메일로 시도해주세요.", visible: true });
setModalState({
isOpen: true,
type: "signup",
});
}
} catch (err) {
console.error(err);
}
}
};
.
.
.
};
export default MailVerify;
로그인/회원가입 모달 상태의 타입 정의
interface accountModalState {
isOpen: boolean;
type: 'login' | 'signup' | 'kakaoSignup' | 'MailVerify' | null;
}
로그인/회원가입 모달창 열림/닫힘 상태
export const accountModalState = atom<accountModalState>({
key: 'accountModalState', // 고유 키
default: {
isOpen: false,
type: null, // 초기값
},
});
이메일 입력 값(이메일 인증 시 사용) 상태
export const emailState = atom<string>({
key: 'emailState', // 고유 키
default: '', // 유저가 입력한 이메일 값
});
useRecoilState로 전역적으로 관리
const [_email, setEmail] = useRecoilState(emailState);
const [_modalState, setModalState] = useRecoilState(accountModalState);
로그인된 상황에서 sendApi를 이용한 모든 요청이 응답 받은 후 거치고 가는 공용 Interceptor함수
- 모든 response header에 authorization, refreshtoken token이 있는지 체크
2-1. 없는 경우 엑세스 토큰 유지 중
2-2. 있는 경우 엑세스 토큰이 만료 되어 리프레시 토큰을 바탕으로 새로운 토큰을 받아온 것, 토큰 교체
- 1003(리프레시 토큰 만료), 1002(빈 토큰)statusCode 인지를 확인
- alert 창 출력 후 로그아웃 처리
axiosInterceptor.interceptors.response.use(
async (response: any) => {
try {
const accessToken = response.headers?.get("authorization");
const refreshToken = response.headers?.get("refreshtoken");
console.log("intercepter: ", accessToken, refreshToken);
if(accessToken !== undefined && refreshToken !== undefined){
sessionStorageService.set("accessToken", accessToken);
sessionStorageService.set("refreshToken", refreshToken);
}
} catch (error) {
console.error("axiosInterceptor error")
}
return response;
},
async (error) => {
try {
if (error.response.data.code === 1003 && error.response.data.code === 1002) {
alert('로그인 유지 시간이 만료되었습니다. 재로그인 해주세요.')
sessionStorageService.delete();
window.location.href = '/'
}
} catch (error) {
console.error("axiosInterceptor error")
}
}
);
서버에 요청을 보내는 공용 함수
- sessionStorage에 accessToken의 유무를 sessionStorageService로 체크
- accessToken이 있다면(로그인한 상태) axiosInterceptor로 토큰 만료 여부 체크
- accessToken이 null이라면 axios요청을 보냄(보통 비로그인시 필요한 요청(ex.닉네임 중복체크)
- 공통적으로 axiosInterceptor을 사용해 요청
- authorization을 요청 헤더에 보냄으로서 axiosInterceptor에서 토큰 만료 등의 여부 체크
export const sendApi = {
get: (url: string) => {
const accessToken = sessionStorageService.get("accessToken");
// accessToken 존재시 즉, 로그인한 상태
if (accessToken !== null) {
return axiosInterceptor.get(
AUTH_KEY.apiUrl + url,
//기본 헤더 설정, 엑세스 토큰 및 리프레시 토큰 설정
authorization()
);
} else {
return axios.get(AUTH_KEY.apiUrl + url, { withCredentials: true });
}
},
post: (url: string, req: object = {}) => {
return axiosInterceptor.post(
AUTH_KEY.apiUrl + url,
req,
authorization()
);
},
put, delete는 post와 동일
요청을 보낼 때 필요한 인증 헤더를 설정
const authorization = () => {
return {
headers: {
Authorization: `${sessionStorageService.get("accessToken")}`,
refreshToken: `${sessionStorageService.get("refreshToken")}`,
},
withCredentials: true,
};
};
세션 스토리지 관리 객체
import { SESSION_ACCESSTOKEN_KEY, SESSION_REFRESHTOKEN_KEY } from "../constants/session";
// SESSION_ACCESSTOKEN_KEY : 엑세스 토큰 key 값 상수 문자열
// SESSION_REFRESHTOKEN_KEY : 리프레시 토큰 key 값 상수 문자열
/**
* 세션 스토리지 관리
*/
const sessionStorageService = {
/**
* 세션 스토리지에 값을 넣는 메소드
* @param type "accessToken" | "refreshToken"
* @param value 받아온 토큰 값
* @returns type과 value를 통해 세션 스토리지에 각각의 토큰을 저장
*/
set: (type: "accessToken" | "refreshToken", value: string = "") => {
if(typeof window !== "undefined"){
if(type === "accessToken"){
return sessionStorage.setItem(SESSION_ACCESSTOKEN_KEY, value)
}else if( type === "refreshToken"){
return sessionStorage.setItem(SESSION_REFRESHTOKEN_KEY, value)
}
}
},
/**
* 세션 스토리지에서 값을 가져오는 메소드
* @param type "accessToken" | "refreshToken"
* @returns type에 맞는 토큰을 반환, 로그인하지 않았을 경우 null
*/
get: (type: "accessToken" | "refreshToken") => {
if(typeof window !== "undefined"){
const data = type ==="accessToken"
? sessionStorage.getItem(SESSION_ACCESSTOKEN_KEY)
: sessionStorage.getItem(SESSION_REFRESHTOKEN_KEY);
if (data) {
return data;
}
return null;
}
},
/**
* 세션 스토리지에서 각각의 토큰을 삭제하는 메소드
* @returns accessToken, refreshToken 토큰 삭제
*/
delete: () => {
return typeof window !== "undefined"
? (sessionStorage.removeItem(SESSION_ACCESSTOKEN_KEY)
,sessionStorage.removeItem(SESSION_REFRESHTOKEN_KEY))
: null;
},
};
export default sessionStorageService;