Skip to content

Commit ccda292

Browse files
authored
develop -> main
Deploy
2 parents c773ee8 + 98d6ac8 commit ccda292

File tree

12 files changed

+408
-6
lines changed

12 files changed

+408
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ dist-ssr
2222
*.njsproj
2323
*.sln
2424
*.sw?
25+
.env

public/_redirects

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* /index.html 200

src/apis/axiosInstance.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import axios from 'axios';
2+
import { refreshAccessToken } from './refreshTokenUtil';
3+
4+
// Axios 인스턴스 생성
5+
const axiosInstance = axios.create({
6+
baseURL: import.meta.env.VITE_SERVER_API, // API 기본 URL을 환경 변수에서 가져옴
7+
//withCredentials: true, // 쿠키를 포함한 요청 허용
8+
});
9+
10+
// 요청 인터셉터
11+
axiosInstance.interceptors.request.use(
12+
(config) => {
13+
// 로컬 스토리지에서 Access Token을 가져와 Authorization 헤더에 추가
14+
const token = localStorage.getItem('access');
15+
if (token) {
16+
config.headers.Authorization = `Bearer ${token}`;
17+
}
18+
return config;
19+
},
20+
(error) => Promise.reject(error)
21+
);
22+
23+
// 응답 인터셉터
24+
axiosInstance.interceptors.response.use(
25+
(response) => response,
26+
async (error) => {
27+
const originalRequest = error.config;
28+
29+
// Access Token이 만료된 경우
30+
if (error.response?.status === 401 && !originalRequest._retry) {
31+
originalRequest._retry = true; // 재시도 플래그 설정
32+
33+
try {
34+
const newAccessToken = await refreshAccessToken();
35+
36+
if (!newAccessToken) {
37+
throw new Error('새로운 Access Token을 받지 못했습니다.');
38+
}
39+
40+
// 새로운 Access Token을 헤더에 추가
41+
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
42+
43+
// 원래 요청을 다시 보냄
44+
return axiosInstance(originalRequest);
45+
} catch (refreshError) {
46+
console.error('토큰 갱신 실패:', refreshError);
47+
48+
// 토큰 갱신 실패 시, 로컬 스토리지에서 토큰 제거 및 로그아웃 처리
49+
localStorage.removeItem('access');
50+
localStorage.removeItem('refresh');
51+
52+
return Promise.reject(refreshError);
53+
}
54+
}
55+
56+
return Promise.reject(error);
57+
}
58+
);
59+
60+
export default axiosInstance;

src/apis/refreshTokenUtil.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import axios from "axios";
2+
3+
export const refreshAccessToken = async () => {
4+
const refreshToken = localStorage.getItem("refresh");
5+
6+
if (!refreshToken) {
7+
console.warn(
8+
"리프레시 토큰이 없습니다. 로그인 상태를 유지할 수 없습니다."
9+
);
10+
return null; // 토큰이 없으면 즉시 null 반환
11+
}
12+
13+
try {
14+
const response = await axios.post(
15+
`${import.meta.env.VITE_SERVER_API}/api/accounts/auth/refresh`,
16+
{
17+
refresh_token: refreshToken,
18+
}
19+
);
20+
21+
const { access_token, refresh_token: newRefreshToken } =
22+
response.data;
23+
24+
// 새로운 토큰 저장
25+
if (access_token) localStorage.setItem("access", access_token);
26+
if (newRefreshToken)
27+
localStorage.setItem("refresh", newRefreshToken);
28+
29+
return access_token; // 새로운 액세스 토큰 반환
30+
} catch (error) {
31+
console.error(
32+
"리프레시 토큰 갱신 실패:",
33+
error.response?.data || error.message
34+
);
35+
36+
// 리프레시 토큰이 유효하지 않으면 로그아웃 처리
37+
localStorage.removeItem("access");
38+
localStorage.removeItem("refresh");
39+
40+
return null;
41+
}
42+
};

src/pages/BasicStructure/Basic.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import TabBar from "../../components/TabBar";
44

55
import Town from "../town/Town";
66
import Place from "../place/Place";
7+
import Course from "../course/Course";
78

89
const Basic = () => {
910
const [activeTab, setActiveTab] = useState("TOWN");
@@ -14,7 +15,7 @@ const Basic = () => {
1415
<S.Content>
1516
{activeTab === "TOWN" && <Town />}
1617
{activeTab === "PLACE" && <Place />}
17-
{activeTab === "COURSE" && <div>코스 관리</div>}
18+
{activeTab === "COURSE" && <Course />}
1819
{activeTab === "TAG" && <div>태그 관리</div>}
1920
{activeTab === "PLACE_APPROVAL" && <div>등록 장소 승인</div>}
2021
{activeTab === "PLACE_REPORT" && <div>장소 제보 확인</div>}

src/pages/course/Course.jsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as S from "./Course.styled";
2+
3+
const Course = () => {
4+
return (
5+
<>
6+
<S.Wrapper>
7+
<S.Title>코스 관리</S.Title>
8+
<S.Container>
9+
<S.Header>
10+
<S.AddBtn>
11+
<span>+</span>
12+
코스 추가
13+
</S.AddBtn>
14+
<S.FilterArea>
15+
<S.Select>
16+
<option value="all">전체</option>
17+
<option value="mangwon">망원</option>
18+
<option value="hongdae">홍대</option>
19+
</S.Select>
20+
</S.FilterArea>
21+
</S.Header>
22+
<S.Table>
23+
<S.TableHeader>
24+
<span>번호</span>
25+
<span>동네 이름</span>
26+
<span>코스명</span>
27+
<span>생성자</span>
28+
<span>활성화</span>
29+
<span>수정</span>
30+
</S.TableHeader>
31+
<S.TableRow>
32+
<span>1</span>
33+
<span>서울</span>
34+
<span>-</span>
35+
<span>태그</span>
36+
<S.Toggle />
37+
<S.EditBtn></S.EditBtn>
38+
</S.TableRow>
39+
<S.TableRow>
40+
<span>2</span>
41+
<span>망원</span>
42+
<span>서울</span>
43+
<span>태그</span>
44+
<S.Toggle />
45+
<S.EditBtn></S.EditBtn>
46+
</S.TableRow>
47+
</S.Table>
48+
</S.Container>
49+
</S.Wrapper>
50+
</>
51+
);
52+
};
53+
54+
export default Course;

src/pages/course/Course.styled.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import styled from "styled-components";
2+
3+
export const Wrapper = styled.div`
4+
display: flex;
5+
flex-direction: column;
6+
justify-content: flex-start;
7+
width: 100vw;
8+
height: 95vh;
9+
margin-top: 5vh;
10+
background-color: ${({ theme}) => theme.colors.white};
11+
`;
12+
13+
export const Title = styled.span`
14+
font-size: 30px;
15+
font-weight: 500;
16+
margin-bottom: 3%;
17+
`;
18+
19+
export const Container = styled.div`
20+
display: flex;
21+
flex-direction: column;
22+
gap: 16px;
23+
width: 80%;
24+
padding: 2%;
25+
border: 3px solid gray;
26+
border-radius: 10px;
27+
`;
28+
29+
export const Header = styled.div`
30+
display: flex;
31+
justify-content: space-between;
32+
align-items: center;
33+
`;
34+
35+
export const FilterArea = styled.div`
36+
display: flex;
37+
gap: 12px;
38+
align-items: center;
39+
`;
40+
41+
export const SearchInput = styled.input`
42+
width: 200px;
43+
padding: 10px 12px;
44+
border: 1px solid #e5e7eb;
45+
border-radius: 6px;
46+
font-size: 14px;
47+
48+
&:focus {
49+
outline: none;
50+
border-color: #111827;
51+
}
52+
`;
53+
54+
export const Select = styled.select`
55+
width: 100px;
56+
padding: 10px;
57+
border: 1px solid #e5e7eb;
58+
border-radius: 6px;
59+
font-size: 14px;
60+
background-color: white;
61+
cursor: pointer;
62+
63+
&:focus {
64+
outline: none;
65+
border-color: #111827;
66+
}
67+
`;
68+
69+
export const AddBtn = styled.button`
70+
display: flex;
71+
align-items: center;
72+
gap: 6px;
73+
74+
padding: 10px 20px;
75+
font-size: 20px;
76+
font-weight: 500;
77+
color: #fff;
78+
79+
background-color: ${({ theme }) => theme.colors.black};
80+
border-radius: 6px;
81+
border: none;
82+
cursor: pointer;
83+
`;
84+
85+
export const Table = styled.div`
86+
width: 100%;
87+
border: 1px solid #e5e7eb;
88+
border-radius: 8px;
89+
overflow: hidden;
90+
`;
91+
92+
export const TableHeader = styled.div`
93+
display: grid;
94+
align-items: center;
95+
justify-items: center;
96+
grid-template-columns: 10% 25% 25% 15% 15% 10%;
97+
padding: 12px;
98+
font-weight: 600;
99+
background-color: #f9fafb;
100+
border-bottom: 1px solid #e5e7eb;
101+
`;
102+
103+
export const TableRow = styled.div`
104+
display: grid;
105+
align-items: center;
106+
justify-items: center;
107+
grid-template-columns: 10% 25% 25% 15% 15% 10%;
108+
109+
padding: 12px;
110+
align-items: center;
111+
border-bottom: 1px solid #e5e7eb;
112+
113+
&:last-child {
114+
border-bottom: none;
115+
}
116+
`;
117+
118+
export const Toggle = styled.div`
119+
width: 36px;
120+
height: 20px;
121+
border-radius: 10px;
122+
background-color: #111827;
123+
position: relative;
124+
cursor: pointer;
125+
126+
&::after {
127+
content: "";
128+
position: absolute;
129+
top: 2px;
130+
left: 18px;
131+
width: 16px;
132+
height: 16px;
133+
background-color: white;
134+
border-radius: 50%;
135+
}
136+
`;
137+
138+
export const EditBtn = styled.button`
139+
display: flex;
140+
justify-content: left;
141+
background: none;
142+
border: none;
143+
cursor: pointer;
144+
font-size: 20px;
145+
`;

src/pages/course/CourseModal.jsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as S from "./CourseModal.styled";
2+
3+
const CourseModal = ({ onClose }) => {
4+
return (
5+
<S.Overlay>
6+
<S.Modal>
7+
<S.Header>
8+
<S.Title>장소 추가</S.Title>
9+
<button onClick={onClose}>x</button>
10+
</S.Header>
11+
</S.Modal>
12+
</S.Overlay>
13+
);
14+
};
15+
16+
export default CourseModal;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import styled from "styled-components";
2+
3+
export const Overlay = styled.div`
4+
position: fixed;
5+
inset: 0;
6+
background: rgba(0, 0, 0, 0.7);
7+
display: flex;
8+
justify-content: center;
9+
align-items: center;
10+
z-index: 1000;
11+
`;
12+
13+
export const Modal = styled.div`
14+
display: flex;
15+
flex-direction: column;
16+
width: 50%;
17+
height: 50%;
18+
background: #fff;
19+
border-radius: 8px;
20+
padding: 20px;
21+
`

0 commit comments

Comments
 (0)