diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml
index 366a9d1..20b5c6a 100644
--- a/.idea/caches/deviceStreaming.xml
+++ b/.idea/caches/deviceStreaming.xml
@@ -124,6 +124,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -146,6 +157,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -212,6 +234,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -278,6 +311,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -399,6 +443,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -597,6 +652,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -641,6 +707,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -686,6 +763,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/(setting)/index.tsx b/app/(setting)/index.tsx
index a1f5c0f..a60b958 100644
--- a/app/(setting)/index.tsx
+++ b/app/(setting)/index.tsx
@@ -2,9 +2,17 @@ import SettinglView from "@/components/view/SettingView";
import { Entypo } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import { Pressable, Text } from "react-native";
+import { authService } from "../services/authService";
export default function SettingScreen() {
const router = useRouter();
+ const handleLogout = async () => {
+ try {
+ await authService.logout();
+ } catch (error) {
+ console.error("설정 화면에서 로그아웃 처리 중 에러:", error);
+ }
+ };
return (
router.push("/(auth)/login")}
+ onPress={handleLogout}
className="flex-row justify-between border-b border-[#B6B6B6] w-full py-6 px-4 active:bg-[#f0f0f0] active:opacity-80"
>
로그아웃
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index b599c47..44103fc 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -5,16 +5,13 @@ import { HapticTab } from "@/components/HapticTab";
import { IconSymbol } from "@/components/ui/IconSymbol";
import TabBarBackground from "@/components/ui/TabBarBackground";
import { Colors } from "@/constants/Colors";
-import { useColorScheme } from "@/hooks/useColorScheme";
import { FontAwesome, MaterialCommunityIcons } from "@expo/vector-icons";
export default function TabLayout() {
- const colorScheme = useColorScheme();
-
return (
+
("/api/v1/member/login", payload, {
headers: {
@@ -37,7 +34,10 @@ export function memberLoginByBody(body: LoginBody) {
throw error;
});
}
-
+export function memberLogout() {
+ //로그아웃
+ return api.post("/api/v1/member/phone/logout");
+}
export function getMemberData() {
//본인 정보 조회
return api.get("/api/v1/member");
diff --git a/app/apis/client.ts b/app/apis/client.ts
index 3cda0a6..5d14bca 100644
--- a/app/apis/client.ts
+++ b/app/apis/client.ts
@@ -1,28 +1,30 @@
import {
- clearTokens /*, getRefreshToken, setTokens*/,
+ clearTokens,
getAccessToken,
+ getRefreshToken,
+ setTokens,
} from "@/app/services/tokenStore";
import axios from "axios";
+import { router } from "expo-router";
const baseURL = process.env.EXPO_PUBLIC_API_BASE_URL;
const api = axios.create({
baseURL,
- timeout: 60000, // 타임아웃 60초로 증가
+ timeout: 60000,
headers: {
- //"Content-Type": "application/json",
Accept: "application/json",
},
});
-// 요청 인터셉터: Bearer 토큰 자동 첨부
+// 요청 인터셉터 (기존과 동일)
api.interceptors.request.use(async (config) => {
const token = await getAccessToken();
const absoluteUrl = config.baseURL
? new URL(config.url ?? "", config.baseURL).toString()
: (config.url ?? "");
if (config.data instanceof FormData) {
- delete (config.headers as any)?.["Content-Type"]; // axios가 자동 세팅
+ delete (config.headers as any)?.["Content-Type"];
}
if (__DEV__) {
console.log(
@@ -39,24 +41,53 @@ api.interceptors.request.use(async (config) => {
}
return config;
});
+interface FailedRequest {
+ resolve: (token: string) => void;
+ reject: (error: any) => void;
+}
+let isRefreshing = false;
+let failedQueue: FailedRequest[] = [];
-// 응답 인터셉터: 401 처리 (필요 시 리프레시 로직 확장 가능)
+const processQueue = (error: any | null, token: string | null) => {
+ failedQueue.forEach((prom) => {
+ if (error) {
+ prom.reject(error);
+ } else if (token) {
+ prom.resolve(token);
+ } else {
+ prom.reject(new Error("토큰 재발급에 성공했으나 토큰이 없습니다."));
+ }
+ });
+ failedQueue = [];
+};
+
+const forceLogout = async () => {
+ await clearTokens();
+ try {
+ router.replace("/(auth)/login");
+ } catch (e) {
+ console.error("로그인 화면 이동 실패", e);
+ }
+};
+
+//응답 인터셉터
api.interceptors.response.use(
(res) => {
- if (__DEV__) {
- console.log(
- "[HTTP Response]",
- res.config.method?.toUpperCase(),
- res.config.url,
- "status:",
- res.status,
- "data:",
- res.data
- );
- }
+ // if (__DEV__) {
+ // console.log(
+ // "[HTTP Response]",
+ // res.config.method?.toUpperCase(),
+ // res.config.url,
+ // "status:",
+ // res.status,
+ // "data:",
+ // res.data
+ // );
+ // }
return res;
},
async (error) => {
+ const originalRequest = error.config;
const status = error?.response?.status;
if (__DEV__) {
@@ -71,14 +102,109 @@ api.interceptors.response.use(
);
}
- // 리프레시 토큰 플로우를 쓰려면 여기서 갱신 후 재시도 로직 추가
+ // 401 에러 처리
if (status === 401) {
- await clearTokens();
- try {
- const { router } = await import("expo-router");
- setTimeout(() => router.replace("/(auth)/login"), 0);
- } catch {}
+ const errorCode = error.response?.data?.error;
+
+ // 재시도 요청이 또 401이면 무한 루프 방지 (기존과 동일)
+ if (originalRequest._retry) {
+ console.error(
+ "토큰 재발급 후 재시도했으나 여전히 401입니다. 로그아웃합니다."
+ );
+ await forceLogout();
+ return Promise.reject(error);
+ }
+
+ switch (errorCode) {
+ case "TOKEN_EXPIRED":
+ console.log("토큰 만료. 재발급을 시도합니다.");
+
+ if (isRefreshing) {
+ return new Promise((resolve, reject) => {
+ failedQueue.push({
+ resolve: (token: string) => {
+ originalRequest.headers.Authorization = `Bearer ${token}`;
+ resolve(api(originalRequest));
+ },
+ reject: (err: any) => {
+ reject(err);
+ },
+ });
+ });
+ }
+
+ originalRequest._retry = true;
+ isRefreshing = true;
+
+ try {
+ const refreshToken = await getRefreshToken();
+ if (!refreshToken) {
+ throw new Error("리프레시 토큰이 없습니다.");
+ }
+ // --- [로그 추가 1] ---
+ console.log(
+ "🔄 [Refresh API] 재발급 요청 전송. 사용할 리프레시 토큰:",
+ refreshToken
+ );
+ //
+ const { data } = await api.post("/api/v1/member/refresh", null, {
+ headers: {
+ Authorization: `Bearer ${refreshToken}`,
+ },
+ });
+ // --- [로그 추가 2] ---
+ console.log(
+ "✅ [Refresh API] 재발급 응답 받음. 새로 발급된 accessToken:",
+ data.accessToken
+ );
+ // ---------------------
+ const newAccessToken = data.accessToken;
+
+ if (!newAccessToken) {
+ throw new Error("서버가 새 액세스 토큰을 반환하지 않았습니다.");
+ }
+
+ await setTokens({
+ accessToken: newAccessToken,
+ refreshToken: refreshToken, // 기존 토큰 유지
+ });
+
+ // api 인스턴스 및 현재 요청 헤더 업데이트
+ api.defaults.headers.common["Authorization"] =
+ `Bearer ${newAccessToken}`;
+ originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
+
+ processQueue(null, newAccessToken); // 큐 처리
+ return api(originalRequest); // 원래 요청 재시도
+ } catch (refreshError) {
+ console.error("토큰 재발급 실패", refreshError);
+ processQueue(refreshError, null);
+ await forceLogout();
+ return Promise.reject(refreshError);
+ } finally {
+ isRefreshing = false;
+ }
+
+ // 즉시 로그아웃 처리 (기존과 동일)
+ case "TOKEN_BLACKLISTED":
+ case "MISSING_TOKEN":
+ case "REFRESH_TOKEN_EXPIRED":
+ case "INVALID_REFRESH_TOKEN":
+ console.warn(`[${errorCode}] - 즉시 로그아웃합니다.`);
+ await forceLogout();
+ return Promise.reject(error);
+
+ // 그 외 401 (기존과 동일)
+ default:
+ console.warn(
+ `처리되지 않은 401 에러 (${errorCode || "N/A"}). 로그아웃합니다.`
+ );
+ await forceLogout();
+ return Promise.reject(error);
+ }
}
+
+ // 401이 아닌 다른 에러 (기존과 동일)
return Promise.reject(error);
}
);
diff --git a/app/services/authService.ts b/app/services/authService.ts
index 0e1d109..984a1aa 100644
--- a/app/services/authService.ts
+++ b/app/services/authService.ts
@@ -1,11 +1,15 @@
-import { memberLoginByBody } from "@/app/apis/auth";
+import { memberLoginByBody, memberLogout } from "@/app/apis/auth";
+import { router } from "expo-router";
import { LoginResponse } from "../apis/auth.type";
import { clearTokens, setTokens } from "./tokenStore";
export const authService = {
async loginWithIdToken(args: { idToken: string }): Promise {
const { data } = await memberLoginByBody(args);
-
+ console.log(
+ "✅ [AuthService] 로그인 응답 데이터:",
+ JSON.stringify(data, null, 2)
+ );
if (!data.accessToken) {
throw new Error(
"로그인에 실패했습니다. 서버로부터 유효한 토큰을 받지 못했습니다."
@@ -19,6 +23,12 @@ export const authService = {
},
async logout() {
+ try {
+ await memberLogout();
+ } catch (error) {
+ console.error("로그아웃 중 에러 발생:", error);
+ }
await clearTokens();
+ router.replace("/(auth)/login");
},
};
diff --git a/app/services/tokenStore.ts b/app/services/tokenStore.ts
index ca6128f..227df8d 100644
--- a/app/services/tokenStore.ts
+++ b/app/services/tokenStore.ts
@@ -1,7 +1,7 @@
import * as SecureStore from "expo-secure-store";
-const ACCESS_TOKEN_KEY = "access_token";
-const REFRESH_TOKEN_KEY = "refresh_token";
+const ACCESS_TOKEN_KEY = "accessToken";
+const REFRESH_TOKEN_KEY = "refreshToken";
let accessTokenCache: string | null = null;
let refreshTokenCache: string | null = null;
@@ -14,8 +14,13 @@ export async function setTokens(tokens: {
await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, tokens.accessToken);
if (tokens.refreshToken) {
+ console.log("🔑 [TokenStore] refreshToken 저장 시도:", tokens.refreshToken);
refreshTokenCache = tokens.refreshToken;
await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, tokens.refreshToken);
+ } else {
+ console.warn(
+ "🔑 [TokenStore] setTokens가 불렸지만 refreshToken이 없습니다."
+ );
}
}
@@ -27,8 +32,14 @@ export async function getAccessToken(): Promise {
}
export async function getRefreshToken(): Promise {
+ console.log(
+ "🔑 [TokenStore] getRefreshToken 호출됨. 캐시:",
+ refreshTokenCache
+ );
if (refreshTokenCache !== null) return refreshTokenCache;
const t = await SecureStore.getItemAsync(REFRESH_TOKEN_KEY);
+ console.log("🔑 [TokenStore] SecureStore에서 읽은 토큰:", t);
+
refreshTokenCache = t;
return t;
}
diff --git a/constants/Colors.ts b/constants/Colors.ts
index 28d9a27..a5232c9 100644
--- a/constants/Colors.ts
+++ b/constants/Colors.ts
@@ -18,7 +18,7 @@ export const Colors = {
},
dark: {
text: "#ECEDEE",
- background: "#151718",
+ background: "#FAFAFA",
tint: tintColorDark,
icon: "#9BA1A6",
tabIconDefault: "#9BA1A6",