Skip to content

Commit ed4e3ec

Browse files
authored
feat:api 수정 (#67)
1 parent be877ee commit ed4e3ec

8 files changed

Lines changed: 134 additions & 184 deletions

File tree

src/apis/instance.ts

Lines changed: 19 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,56 @@
1+
// src/apis/instance.ts
12
import axios, {
23
AxiosError,
34
AxiosHeaders,
45
InternalAxiosRequestConfig,
56
AxiosRequestHeaders,
6-
AxiosRequestConfig,
77
} from "axios";
88
import useTokenStore from "@/stores/useTokenStore";
9-
import { GlobalResponse } from "@/types/common/apiResponse.type";
109

11-
// ===== 타입 정의 =====
12-
interface RetryConfig extends InternalAxiosRequestConfig {
13-
_retry?: boolean;
10+
type ReqConfig = InternalAxiosRequestConfig & {
1411
_skipAuth?: boolean;
15-
}
16-
17-
interface RefreshRequestConfig extends AxiosRequestConfig {
18-
_skipAuth?: boolean;
19-
}
20-
21-
interface RefreshResponse {
22-
accessToken: string;
23-
refreshToken: string;
24-
}
12+
};
2513

26-
// ===== API 인스턴스 =====
2714
const api = axios.create({
2815
baseURL: import.meta.env.VITE_API_URL,
2916
});
3017

31-
// 리프레시 전용 클라이언트(인터셉터 X)
32-
const refreshClient = axios.create({
33-
baseURL: import.meta.env.VITE_API_URL,
34-
});
35-
36-
/** 요청 인터셉터: 최신 accessToken 자동 부착 */
37-
api.interceptors.request.use((config: RetryConfig) => {
38-
// 스킵 옵션이면 바로 리턴
18+
/** 요청 인터셉터: accessToken 부착 (옵션으로 스킵 가능) */
19+
api.interceptors.request.use((config: ReqConfig) => {
3920
if (config._skipAuth) return config;
4021

4122
const { accessToken } = useTokenStore.getState();
4223
const headers = (config.headers ??=
4324
new AxiosHeaders()) as AxiosRequestHeaders;
4425

45-
if (accessToken) {
46-
headers.Authorization = `Bearer ${accessToken}`;
47-
} else {
48-
delete headers.Authorization;
49-
}
26+
if (accessToken) headers.Authorization = `Bearer ${accessToken}`;
27+
else delete headers.Authorization;
28+
5029
return config;
5130
});
5231

53-
// ===== 토큰 갱신 동시성 제어 =====
54-
let isRefreshing = false;
55-
let queue: Array<(token: string | null) => void> = [];
56-
const flushQueue = (token: string | null) => {
57-
queue.forEach(resolve => resolve(token));
58-
queue = [];
59-
};
60-
61-
const isRefreshUrl = (url?: string) => url?.includes("/api/v1/auth/refresh");
62-
63-
/** 응답 인터셉터: 401 → refresh → 재시도 */
32+
/** 응답 인터셉터: 403 → /onboarding 이동, 401 → 토큰 클리어 */
6433
api.interceptors.response.use(
6534
res => res,
6635
async (error: AxiosError) => {
6736
const status = error.response?.status;
68-
const original = error.config as RetryConfig | undefined;
69-
70-
// config 없는 경우 or 401 아님 → 바로 reject
71-
if (!original || status !== 401) {
72-
return Promise.reject(error);
73-
}
7437

75-
// 리프레시 요청 자체가 401이면 → 바로 종료
76-
if (isRefreshUrl(original.url)) {
77-
flushQueue(null);
78-
useTokenStore.getState().clearTokens();
38+
if (status === 403) {
39+
// 권한 없음 → 온보딩으로 보냄
40+
window.location.assign("/onboarding");
41+
// 이동 직후에도 호출부는 에러 흐름 유지
7942
return Promise.reject(error);
8043
}
8144

82-
// 무한루프 방지
83-
if (original._retry) {
45+
if (status === 401) {
46+
// 인증 만료/무효 → 토큰 정리 (필요시 로그인 페이지로 이동하도록 조정 가능)
47+
const store = useTokenStore.getState();
48+
store.clearTokens?.();
49+
// 예: window.location.assign("/login");
8450
return Promise.reject(error);
8551
}
86-
original._retry = true;
87-
88-
const store = useTokenStore.getState();
89-
const refreshToken = store.refreshToken;
90-
91-
// 리프레시 토큰 없으면 → 로그아웃
92-
if (!refreshToken) {
93-
store.clearTokens();
94-
return Promise.reject(error);
95-
}
96-
97-
// 이미 리프레시 중이면 완료까지 대기
98-
if (isRefreshing) {
99-
const newToken = await new Promise<string | null>(resolve => {
100-
queue.push(resolve);
101-
});
102-
if (!newToken) {
103-
store.clearTokens();
104-
return Promise.reject(error);
105-
}
106-
const headers = (original.headers ??=
107-
new AxiosHeaders()) as AxiosRequestHeaders;
108-
headers.Authorization = `Bearer ${newToken}`;
109-
return api(original);
110-
}
11152

112-
// 실제 리프레시 요청 수행
113-
try {
114-
isRefreshing = true;
115-
116-
const resp = await refreshClient.post<GlobalResponse<RefreshResponse>>(
117-
"/api/v1/auth/refresh",
118-
{ refreshToken },
119-
{ headers: new AxiosHeaders(), _skipAuth: true } as RefreshRequestConfig
120-
);
121-
122-
const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
123-
resp.data.result;
124-
125-
if (!newAccessToken) {
126-
throw new Error("No accessToken in refresh response");
127-
}
128-
129-
// 스토어 갱신
130-
store.setAccessToken(newAccessToken);
131-
if (newRefreshToken) {
132-
store.setRefreshToken(newRefreshToken);
133-
}
134-
135-
// 대기 중인 요청 처리
136-
flushQueue(newAccessToken);
137-
138-
// 원 요청 재시도
139-
const headers = (original.headers ??=
140-
new AxiosHeaders()) as AxiosRequestHeaders;
141-
headers.Authorization = `Bearer ${newAccessToken}`;
142-
return api(original);
143-
} catch (e) {
144-
flushQueue(null);
145-
useTokenStore.getState().clearTokens();
146-
return Promise.reject(e);
147-
} finally {
148-
isRefreshing = false;
149-
}
53+
return Promise.reject(error);
15054
}
15155
);
15256

src/components/bottom-sheet/BottomSheet.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ const BottomSheet: React.FC<{ setIsModalOpen: (isOpen: boolean) => void }> = ({
1414
const { data } = usePanelApi();
1515
console.log(data);
1616
// ===== 표시용 상태 =====
17-
const [isChecked] = useState(data?.isCheckingCompleted);
18-
const [isChecked2] = useState(data?.isDairyCompleted);
19-
const [isChecked3] = useState(data?.isQuizCompleted);
17+
const [isChecked, setIsChecked] = useState(data?.isCheckingCompleted);
18+
const [isChecked2, setIsChecked2] = useState(data?.isDairyCompleted);
19+
const [isChecked3, setIsChecked3] = useState(data?.isQuizCompleted);
2020
const [percent] = useState(data?.wishTree.progressPercent);
2121
const randomNum = (Math.floor(Math.random() * 3) + 1) % 2;
2222
const path =
@@ -27,6 +27,11 @@ const BottomSheet: React.FC<{ setIsModalOpen: (isOpen: boolean) => void }> = ({
2727
// ===== 스냅/드래그 파라미터 =====
2828

2929
// 스냅 완료 상태를 보관(초기엔 닫힘 위치로 시작)
30+
useEffect(() => {
31+
setIsChecked(data?.isCheckingCompleted);
32+
setIsChecked2(data?.isDairyCompleted);
33+
setIsChecked3(data?.isQuizCompleted);
34+
}, [data]);
3035

3136
// ===== DOM/드래그 제어용 ref =====
3237
const rafId = useRef<number | null>(null);

src/components/common/Toast.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function Toast({
2424

2525
return (
2626
<div
27-
className={`z-60 flex justify-between items-center absolute top-0 left-1/2 w-[90vw] -translate-x-1/2 rounded-sm text-black px-4 py-2 bg-white transition-all duration-300 ${
27+
className={`z-60 flex justify-between items-center fixed top-10 left-1/2 w-[90vw] -translate-x-1/2 rounded-sm text-black px-4 py-2 bg-white transition-all duration-300 ${
2828
visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-3"
2929
}`}
3030
>

src/components/home/Avatar.tsx

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import BirdModal from "./BirdModal";
88
import Modal from "./Modal";
99

1010
import Toast from "../common/Toast";
11-
11+
import { useHomeSummaryStore } from "@/stores/useGardenStore";
1212

1313
const Avatar = ({
1414
isWater,
@@ -21,15 +21,19 @@ const Avatar = ({
2121
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
2222
isOpen: boolean;
2323
}) => {
24+
const { missions } = useHomeSummaryStore();
25+
2426
const [isChecked, setIsChecked] = useState<number>(0);
2527
const [isOpenBird, setIsOpenBird] = useState(false);
2628
const [isToastOpen, setIsToastOpen] = useState(false);
2729
const [isToastOpen2, setIsToastOpen2] = useState(false);
2830

31+
const [isAnswered, setIsAnswered] = useState(missions[2].completed);
32+
2933
useEffect(() => {
3034
if (isChecked === 1 || isChecked === 2 || isChecked === 3) {
3135
const timer = setTimeout(() => {
32-
setIsChecked(4);
36+
setIsAnswered(true);
3337
}, 2000); // 3초
3438

3539
// cleanup: 상태가 바뀌거나 컴포넌트 언마운트 시 타이머 해제
@@ -38,15 +42,15 @@ const Avatar = ({
3842
}, [isChecked]);
3943

4044
const handleClick = () => {
41-
if (isChecked === 0) {
45+
if (isAnswered === false) {
4246
setIsToastOpen(true);
4347
}
4448
};
4549

4650
return (
4751
<div
4852
className={`flex-1 flex items-center justify-center relative w-full max-w-md ${
49-
isChecked === 0 ? "z-40" : "z-10"
53+
isAnswered ? "z-10" : "z-40"
5054
}`}
5155
onClick={handleClick}
5256
>
@@ -72,7 +76,7 @@ const Avatar = ({
7276
alt="bird"
7377
className="absolute bottom-0 right-20 w-24 h-auto"
7478
onClick={() => {
75-
if (isChecked === 0) {
79+
if (isAnswered === false) {
7680
setIsToastOpen(true);
7781
} else {
7882
setIsOpenBird(true);
@@ -82,10 +86,10 @@ const Avatar = ({
8286
<div className="balloon-wrapper text-body-sb z-45">
8387
<>
8488
<div
85-
className={`balloon balloon-center text-body-sb text-black gap-4 flex flex-col py-5 px-6 min-w-50 ${
86-
isChecked === 4
87-
? "bg-transparent after:border-transparent"
88-
: "bg-white after:border-l-transparent after:border-b-transparent after:border-r-transparent after:border-t-white"
89+
className={`balloon balloon-center text-body-sb gap-4 flex flex-col py-5 px-6 min-w-50 ${
90+
isAnswered
91+
? "bg-transparent after:border-transparent text-transparent"
92+
: "text-black bg-white after:border-l-transparent after:border-b-transparent after:border-r-transparent after:border-t-white"
8993
}`}
9094
onClick={e => {
9195
e.stopPropagation();
@@ -96,23 +100,29 @@ const Avatar = ({
96100
e.preventDefault();
97101
}}
98102
>
99-
{isChecked === 0 && (
103+
{!isAnswered && (
100104
<>
101-
<div>
102-
오늘도 만나서 정말 반가워요!
103-
<br />
104-
괜찮으시다면 오늘 하루는 어떠셨는지
105-
<br />
106-
살짝 알려주시겠어요?
107-
</div>
108-
<button
109-
onClick={() => {
110-
setIsModalOpen(true);
111-
}}
112-
className="button-primary"
105+
<div
106+
className={`${
107+
isChecked > 0 ? "hidden" : "block"
108+
} text-black`}
113109
>
114-
마음 건강 체크
115-
</button>
110+
<div>
111+
오늘도 만나서 정말 반가워요!
112+
<br />
113+
괜찮으시다면 오늘 하루는 어떠셨는지
114+
<br />
115+
살짝 알려주시겠어요?
116+
</div>
117+
<button
118+
onClick={() => {
119+
setIsModalOpen(true);
120+
}}
121+
className="button-primary"
122+
>
123+
마음 건강 체크
124+
</button>
125+
</div>
116126
</>
117127
)}
118128
{isChecked === 1 && <>좋은 기분으로 오늘 하루 계속 이어가요!</>}
@@ -137,6 +147,8 @@ const Avatar = ({
137147
setIsOpen={setIsModalOpen}
138148
setIsChecked={setIsChecked}
139149
isChecked={isChecked}
150+
setIsAnswered={setIsAnswered}
151+
isAnswered={isAnswered}
140152
/>
141153
)}
142154
{isToastOpen && (

0 commit comments

Comments
 (0)