From d42c72cab75c0efd91c2013aab5fd6ed60d28bdc Mon Sep 17 00:00:00 2001 From: YousupLim <99yousup@gachon.ac.kr> Date: Wed, 19 Nov 2025 00:11:27 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore:=208=EC=A3=BC=EC=B0=A8=20PR=EC=9A=A9?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\354\236\204\354\234\240\354\204\255/my-app/src/main.jsx" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/main.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/main.jsx" index caa554a..f1fe87a 100644 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/main.jsx" +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/main.jsx" @@ -4,7 +4,7 @@ import { BrowserRouter } from "react-router-dom"; import { AuthProvider } from "./context/AuthContext"; import App from "./App"; import "./index.css"; - +//새로 준비상황 ReactDOM.createRoot(document.getElementById("root")).render( From 1a65b80a3e8259b9c7c729ad2e82d8655345778a Mon Sep 17 00:00:00 2001 From: YousupLim <99yousup@gachon.ac.kr> Date: Wed, 19 Nov 2025 00:13:05 +0900 Subject: [PATCH 2/5] =?UTF-8?q?chore:=208=EC=A3=BC=EC=B0=A8=20PR=EC=9A=A9?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../my-app/src/oauth/kakao.js" | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/oauth/kakao.js" diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/oauth/kakao.js" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/oauth/kakao.js" new file mode 100644 index 0000000..910a8f2 --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/oauth/kakao.js" @@ -0,0 +1,80 @@ +// src/oauth/kakao.js +const KAKAO_REST_API_KEY = import.meta.env.VITE_KAKAO_REST_API_KEY; +const KAKAO_REDIRECT_URI = import.meta.env.VITE_KAKAO_REDIRECT_URI; + +// 1) 카카오 authorize URL 만들기 +export const getKakaoAuthUrl = () => { + const params = new URLSearchParams({ + client_id: KAKAO_REST_API_KEY, + redirect_uri: KAKAO_REDIRECT_URI, + response_type: "code", + }); + + return `https://kauth.kakao.com/oauth/authorize?${params.toString()}`; +}; + +// 2) 인가 코드 → 액세스 토큰 +export const getKakaoToken = async (code) => { + const params = new URLSearchParams({ + grant_type: "authorization_code", + client_id: KAKAO_REST_API_KEY, + redirect_uri: KAKAO_REDIRECT_URI, + code, + }); + + const res = await fetch("https://kauth.kakao.com/oauth/token", { + method: "POST", + headers: { + "Content-type": "application/x-www-form-urlencoded;charset=utf-8", + }, + body: params, + }); + + if (!res.ok) { + throw new Error("카카오 토큰 요청 실패"); + } + + return res.json(); // { access_token, refresh_token, ... } +}; + +// 3) 액세스 토큰 → 카카오 유저 정보 +export const getKakaoUserInfo = async (accessToken) => { + const res = await fetch("https://kapi.kakao.com/v2/user/me", { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-type": "application/x-www-form-urlencoded;charset=utf-8", + }, + }); + + if (!res.ok) { + throw new Error("카카오 사용자 정보 요청 실패"); + } + + return res.json(); // { id, kakao_account: { profile, email, ... } } +}; +4. Kakao 로그인 버튼 컴포넌트 +위치 예시: src/components/KakaoLoginButton.jsx + +jsx +코드 복사 +// src/components/KakaoLoginButton.jsx +import { getKakaoAuthUrl } from "../oauth/kakao"; + +export default function KakaoLoginButton() { + const handleKakaoLogin = () => { + const authUrl = getKakaoAuthUrl(); + window.location.href = authUrl; // 카카오 로그인 페이지로 리다이렉트 + }; + + return ( + + ); +} \ No newline at end of file From bcd35b6ec8f739566c00d6efa3c49f8b2cff59e7 Mon Sep 17 00:00:00 2001 From: YousupLim <99yousup@gachon.ac.kr> Date: Wed, 26 Nov 2025 00:30:59 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B0=8F=20TODO=20=EB=B3=B4?= =?UTF-8?q?=ED=98=B8=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../my-app/.env" | 2 + .../my-app/package-lock.json" | 27 +++++ .../my-app/package.json" | 7 +- .../my-app/src/App.jsx" | 53 +++++---- .../my-app/src/api/authApi.js" | 68 +++++++++++ .../my-app/src/api/client.js" | 40 +++++++ .../my-app/src/api/todoApi.js" | 16 +++ .../src/components/KakaoLoginButton.jsx" | 20 ++++ .../my-app/src/components/ProtectedRoute.jsx" | 15 +++ .../my-app/src/main.jsx" | 21 ++-- .../my-app/src/oauth/kakao.js" | 80 ------------- .../my-app/src/pages/KakaoCallbackPage.jsx" | 68 +++++++++++ .../my-app/src/pages/Login.jsx" | 41 ------- .../my-app/src/pages/LoginPage.jsx" | 95 ++++++++++++++++ .../my-app/src/pages/SignUpPage.jsx" | 107 ++++++++++++++++++ .../my-app/src/pages/Signup.jsx" | 37 ------ .../my-app/src/pages/TodoPage.jsx" | 63 ++++++++++- 17 files changed, 563 insertions(+), 197 deletions(-) create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/.env" create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/authApi.js" create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/client.js" create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/todoApi.js" create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/components/KakaoLoginButton.jsx" create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/components/ProtectedRoute.jsx" delete mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/oauth/kakao.js" create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/KakaoCallbackPage.jsx" delete mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/Login.jsx" create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/LoginPage.jsx" create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/SignUpPage.jsx" delete mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/Signup.jsx" diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/.env" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/.env" new file mode 100644 index 0000000..55fc0d4 --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/.env" @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=https://blog.leets.land +VITE_KAKAO_REDIRECT_PATH=/oauth/kakao \ No newline at end of file diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/package-lock.json" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/package-lock.json" index d647e42..f8aa62a 100644 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/package-lock.json" +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/package-lock.json" @@ -8,6 +8,7 @@ "name": "my-app", "version": "0.0.0", "dependencies": { + "@tanstack/react-query": "^5.90.10", "axios": "^1.13.2", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -846,6 +847,32 @@ "win32" ] }, + "node_modules/@tanstack/query-core": { + "version": "5.90.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.10.tgz", + "integrity": "sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.10.tgz", + "integrity": "sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tinyhttp/accepts": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@tinyhttp/accepts/-/accepts-2.2.3.tgz", diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/package.json" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/package.json" index 5d17946..6ccf67f 100644 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/package.json" +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/package.json" @@ -4,12 +4,13 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", + "dev": "vite --port 3000", + "build": "vite build", + "preview": "vite preview", "dev:server": "json-server --watch db.json --port 4000" }, "dependencies": { + "@tanstack/react-query": "^5.90.10", "axios": "^1.13.2", "react": "^19.1.1", "react-dom": "^19.1.1", diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/App.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/App.jsx" index 77cfe77..f060b20 100644 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/App.jsx" +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/App.jsx" @@ -1,29 +1,32 @@ -import { Routes, Route } from "react-router-dom"; -import Header from "./components/Header"; -import Login from "./pages/Login"; -import Register from "./pages/Register"; -import PrivateRoute from "./components/PrivateRoute"; - -function Home() { - return
로그인된 사용자만 볼 수 있는 홈 화면
; -} +// src/App.jsx +import { Routes, Route, Navigate } from "react-router-dom"; +import LoginPage from "./pages/LoginPage"; +import SignUpPage from "./pages/SignUpPage"; +import TodoPage from "./pages/TodoPage"; +import KakaoCallbackPage from "./pages/KakaoCallbackPage"; +import ProtectedRoute from "./components/ProtectedRoute"; export default function App() { return ( -
-
- - - - - } - /> - } /> - } /> - -
+ + } /> + } /> + } /> + + {/* 카카오 콜백 */} + } + /> + + + + + } + /> + ); -} \ No newline at end of file +} diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/authApi.js" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/authApi.js" new file mode 100644 index 0000000..c3a12e5 --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/authApi.js" @@ -0,0 +1,68 @@ +// src/api/authApi.js +import apiClient, { BASE_URL } from "./client"; + +// 🔸 카카오 로그인 버튼에서 사용할 URL (백엔드 → 카카오로 다시 리다이렉트) +export const getKakaoLoginUrl = () => { + return `${BASE_URL}/auth/kakao`; + // Swagger 보고 /auth/kakao 경로가 다르면 여기만 바꿔 주면 됨 +}; + +// 🔸 콜백 페이지에서 호출: /auth/kakao/redirect?code=... +export const kakaoRedirect = async (code) => { + const res = await apiClient.get("/auth/kakao/redirect", { + params: { code }, + }); + + // 실제 응답 구조는 Swagger에서 확인해서 맞춰야 하지만, + // 기본 형태를 이런 식으로 가정해 둔 거야. + const { accessToken, user } = res.data; + + if (accessToken) { + localStorage.setItem("accessToken", accessToken); + } + if (user) { + localStorage.setItem("user", JSON.stringify(user)); + } + + return user; +}; + +// 🔸 일반 로그인 +export const login = async ({ email, password }) => { + const res = await apiClient.post("/auth/login", { email, password }); + // 실제 경로와 응답 구조를 Swagger에서 확인해서 맞춰줘야 함 + const { accessToken, user } = res.data; + + if (accessToken) { + localStorage.setItem("accessToken", accessToken); + } + if (user) { + localStorage.setItem("user", JSON.stringify(user)); + } + + return user; +}; + +// 🔸 회원가입 +export const signUp = async ({ email, password, nickname }) => { + const res = await apiClient.post("/auth/signup", { + email, + password, + nickname, + }); + + // 필요하면 이쪽에서도 토큰/유저 저장해 줄 수 있음 + return res.data; +}; + +// 🔸 로그아웃 (선택) +export const logout = async () => { + try { + await apiClient.post("/auth/logout"); // 없으면 에러 나도 catch에서 무시 + } catch (e) { + // ignore + } finally { + localStorage.removeItem("accessToken"); + localStorage.removeItem("user"); + } +}; diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/client.js" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/client.js" new file mode 100644 index 0000000..12f1b1c --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/client.js" @@ -0,0 +1,40 @@ +// src/api/client.js +import axios from "axios"; + +export const BASE_URL = import.meta.env.VITE_API_BASE_URL; + +// axios 인스턴스 생성 +const apiClient = axios.create({ + baseURL: BASE_URL, + withCredentials: true, // 백엔드가 쿠키를 쓴다면 필요 +}); + +// 요청 인터셉터: accessToken 있으면 Authorization 헤더에 자동 첨부 +apiClient.interceptors.request.use((config) => { + const token = localStorage.getItem("accessToken"); + + if (token && !config.headers.Authorization) { + config.headers.Authorization = `Bearer ${token}`; + } + + return config; +}); + +// 응답 인터셉터: 401 나오면 토큰 삭제 (로그아웃 상태로 정리) +apiClient.interceptors.response.use( + (response) => response, + (error) => { + const status = error.response?.status; + + if (status === 401) { + localStorage.removeItem("accessToken"); + localStorage.removeItem("user"); + // 여기에서 바로 window.location.href = "/login" 해도 되고, + // 화면 쪽에서 에러를 받아서 처리해도 됨. + } + + return Promise.reject(error); + } +); + +export default apiClient; diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/todoApi.js" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/todoApi.js" new file mode 100644 index 0000000..bf1615e --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/todoApi.js" @@ -0,0 +1,16 @@ +// src/api/todoApi.js +import apiClient from "./client"; + +// TODO 목록 조회 +export const fetchTodos = async () => { + const res = await apiClient.get("/todos"); // 실제 경로 확인 + return res.data; +}; + +// TODO 추가 +export const createTodo = async (title) => { + const res = await apiClient.post("/todos", { title }); // 실제 경로 확인 + return res.data; +}; + +// TODO 완료 토글 등 나중에 필요하면 추가 diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/components/KakaoLoginButton.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/components/KakaoLoginButton.jsx" new file mode 100644 index 0000000..5d6fb3f --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/components/KakaoLoginButton.jsx" @@ -0,0 +1,20 @@ +// src/components/KakaoLoginButton.jsx +import { getKakaoLoginUrl } from "../api/authApi"; + +export default function KakaoLoginButton() { + const handleClick = () => { + const url = getKakaoLoginUrl(); + // 과제 설명에 있는 window.location.ref 는 오타로 보고 href 사용 + window.location.href = url; + }; + + return ( + + ); +} diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/components/ProtectedRoute.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/components/ProtectedRoute.jsx" new file mode 100644 index 0000000..2ecdfea --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/components/ProtectedRoute.jsx" @@ -0,0 +1,15 @@ +// src/components/ProtectedRoute.jsx +import { Navigate } from "react-router-dom"; + +export default function ProtectedRoute({ children }) { + const token = localStorage.getItem("accessToken"); + const user = localStorage.getItem("user"); + + if (!token || !user) { + // 로그인 안 되어 있으면 로그인 페이지로 이동 + return ; + } + + // 로그인 되어있으면 원래 페이지 렌더 + return children; +} diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/main.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/main.jsx" index f1fe87a..08340cc 100644 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/main.jsx" +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/main.jsx" @@ -1,14 +1,19 @@ +// src/main.jsx import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; -import { AuthProvider } from "./context/AuthContext"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import App from "./App"; import "./index.css"; -//새로 준비상황 + +const queryClient = new QueryClient(); + ReactDOM.createRoot(document.getElementById("root")).render( - - - - - -); \ No newline at end of file + + + + + + + +); diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/oauth/kakao.js" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/oauth/kakao.js" deleted file mode 100644 index 910a8f2..0000000 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/oauth/kakao.js" +++ /dev/null @@ -1,80 +0,0 @@ -// src/oauth/kakao.js -const KAKAO_REST_API_KEY = import.meta.env.VITE_KAKAO_REST_API_KEY; -const KAKAO_REDIRECT_URI = import.meta.env.VITE_KAKAO_REDIRECT_URI; - -// 1) 카카오 authorize URL 만들기 -export const getKakaoAuthUrl = () => { - const params = new URLSearchParams({ - client_id: KAKAO_REST_API_KEY, - redirect_uri: KAKAO_REDIRECT_URI, - response_type: "code", - }); - - return `https://kauth.kakao.com/oauth/authorize?${params.toString()}`; -}; - -// 2) 인가 코드 → 액세스 토큰 -export const getKakaoToken = async (code) => { - const params = new URLSearchParams({ - grant_type: "authorization_code", - client_id: KAKAO_REST_API_KEY, - redirect_uri: KAKAO_REDIRECT_URI, - code, - }); - - const res = await fetch("https://kauth.kakao.com/oauth/token", { - method: "POST", - headers: { - "Content-type": "application/x-www-form-urlencoded;charset=utf-8", - }, - body: params, - }); - - if (!res.ok) { - throw new Error("카카오 토큰 요청 실패"); - } - - return res.json(); // { access_token, refresh_token, ... } -}; - -// 3) 액세스 토큰 → 카카오 유저 정보 -export const getKakaoUserInfo = async (accessToken) => { - const res = await fetch("https://kapi.kakao.com/v2/user/me", { - method: "GET", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-type": "application/x-www-form-urlencoded;charset=utf-8", - }, - }); - - if (!res.ok) { - throw new Error("카카오 사용자 정보 요청 실패"); - } - - return res.json(); // { id, kakao_account: { profile, email, ... } } -}; -4. Kakao 로그인 버튼 컴포넌트 -위치 예시: src/components/KakaoLoginButton.jsx - -jsx -코드 복사 -// src/components/KakaoLoginButton.jsx -import { getKakaoAuthUrl } from "../oauth/kakao"; - -export default function KakaoLoginButton() { - const handleKakaoLogin = () => { - const authUrl = getKakaoAuthUrl(); - window.location.href = authUrl; // 카카오 로그인 페이지로 리다이렉트 - }; - - return ( - - ); -} \ No newline at end of file diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/KakaoCallbackPage.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/KakaoCallbackPage.jsx" new file mode 100644 index 0000000..24633ac --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/KakaoCallbackPage.jsx" @@ -0,0 +1,68 @@ +// src/pages/KakaoCallbackPage.jsx +import { useEffect } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { kakaoRedirect } from "../api/authApi"; + +export default function KakaoCallbackPage() { + const location = useLocation(); + const navigate = useNavigate(); + + // URL 에서 ?code=... 꺼내기 + const searchParams = new URLSearchParams(location.search); + const code = searchParams.get("code"); + + // 인가 코드가 없으면 바로 에러 문구 + if (!code) { + return
인가 코드가 없습니다. 다시 로그인해 주세요.
; + } + + const { data, error, isLoading, isError } = useQuery({ + queryKey: ["kakaoRedirect", code], + queryFn: () => kakaoRedirect(code), + enabled: !!code, + retry: false, + }); + + // 👉 에러가 났을 때 과제 스펙 처리 + useEffect(() => { + if (!isError || !error) return; + + const status = error?.response?.status; + + // 6-1. 회원가입 필요 -> code 401 + if (status === 401) { + navigate("/signup?from=kakao", { replace: true }); + return; + } + + // 그 외 상태코드는 일단 콘솔에 찍어두기 + console.log("카카오 리다이렉트 에러", status, error?.response?.data); + }, [isError, error, navigate]); + + // 👉 성공 시 TODO 페이지로 이동 + useEffect(() => { + if (!data) return; + + // 7. 로그인/회원가입 성공 -> code 200 으로 왔다고 가정 + navigate("/todos", { replace: true }); + }, [data, navigate]); + + if (isLoading) { + return
카카오 로그인 처리 중...
; + } + + // 401인 경우에는 위 useEffect에서 /signup 으로 이동하면서 여기 렌더는 거의 안 보일 거고, + // 401이 아닌 다른 에러는 아래 메시지가 보이게 된다. + if (isError) { + const status = error?.response?.status; + return ( +
+ 로그인 중 오류가 발생했습니다. (status: {status ?? "알 수 없음"}) +
+ ); + } + + // data 가 도착하면 위 useEffect에서 /todos 로 이동하므로 여기도 거의 안 보임 + return
카카오 로그인 처리 중...
; +} diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/Login.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/Login.jsx" deleted file mode 100644 index 28d48b4..0000000 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/Login.jsx" +++ /dev/null @@ -1,41 +0,0 @@ -import { useState } from "react"; -import { useAuth } from "../context/AuthContext"; -import { useNavigate } from "react-router-dom"; - -export default function Login() { - const { login } = useAuth(); - const nav = useNavigate(); - const [form, setForm] = useState({ username: "", password: "" }); - const [msg, setMsg] = useState(null); - - const onChange = (e) => setForm((f) => ({ ...f, [e.target.name]: e.target.value })); - - const onSubmit = async (e) => { - e.preventDefault(); - setMsg(null); - - const res = await login(form); - - if (!res.ok) { - // 에러 메시지를 상황별로 다르게 - if (res.code === "USER_NOT_FOUND") setMsg("가입되지 않은 아이디입니다."); - else if (res.code === "INVALID_PASSWORD") setMsg("비밀번호가 올바르지 않습니다."); - else setMsg("로그인에 실패했어요. 잠시 후 다시 시도해 주세요."); - return; - } - - nav("/"); // 로그인 성공 시 홈으로 - }; - - return ( -
-

로그인

-
- - - -
- {msg &&

{msg}

} -
- ); -} diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/LoginPage.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/LoginPage.jsx" new file mode 100644 index 0000000..e908221 --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/LoginPage.jsx" @@ -0,0 +1,95 @@ +// src/pages/LoginPage.jsx +import { useState } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { useMutation } from "@tanstack/react-query"; +import { login } from "../api/authApi"; +import KakaoLoginButton from "../components/KakaoLoginButton"; + +export default function LoginPage() { + const navigate = useNavigate(); + + const [form, setForm] = useState({ + email: "", + password: "", + }); + + const { mutate, isPending, error } = useMutation({ + mutationFn: login, + onSuccess: () => { + navigate("/todos"); + }, + }); + + const handleChange = (e) => { + const { name, value } = e.target; + setForm((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + mutate(form); + }; + + return ( +
+

로그인

+ +
+
+ + +
+ +
+ + +
+ + {error && ( +

+ 로그인에 실패했습니다. 이메일/비밀번호를 확인해 주세요. +

+ )} + + +
+ + {/* 구분선 */} +
+
+ 또는 +
+
+ + {/* 카카오 로그인 버튼 */} + + +

+ 아직 계정이 없나요?{" "} + + 회원가입 + +

+
+ ); +} diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/SignUpPage.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/SignUpPage.jsx" new file mode 100644 index 0000000..7d72677 --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/SignUpPage.jsx" @@ -0,0 +1,107 @@ +// src/pages/SignUpPage.jsx +import { useState } from "react"; +import { useLocation, useNavigate, Link } from "react-router-dom"; +import { useMutation } from "@tanstack/react-query"; +import { signUp } from "../api/authApi"; + +export default function SignUpPage() { + const navigate = useNavigate(); + const location = useLocation(); + const searchParams = new URLSearchParams(location.search); + const fromKakao = searchParams.get("from") === "kakao"; + + const [form, setForm] = useState({ + email: "", + password: "", + nickname: "", + }); + + const { mutate, isPending, error } = useMutation({ + mutationFn: signUp, + onSuccess: () => { + // 회원가입 후 로그인 페이지로 + navigate("/login"); + }, + }); + + const handleChange = (e) => { + const { name, value } = e.target; + setForm((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + mutate(form); + }; + + return ( +
+

회원가입

+ + {fromKakao && ( +

+ 카카오 로그인은 완료되었지만, 우리 서비스에는 아직 계정이 없어 + 추가 정보 입력 후 회원가입이 필요합니다. +

+ )} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + {error && ( +

+ 회원가입에 실패했습니다. 입력값을 다시 확인해 주세요. +

+ )} + + +
+ +

+ 이미 계정이 있나요?{" "} + + 로그인으로 돌아가기 + +

+
+ ); +} diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/Signup.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/Signup.jsx" deleted file mode 100644 index e8bd6ea..0000000 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/Signup.jsx" +++ /dev/null @@ -1,37 +0,0 @@ -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import Header from "../components/Header.jsx"; - -export default function Signup() { - const [form, setForm] = useState({ name: "", id: "", pw: "" }); - const navigate = useNavigate(); - - const onChange = (e) => { - const { name, value } = e.target; - setForm((s) => ({ ...s, [name]: value })); - }; - - const onSubmit = (e) => { - e.preventDefault(); - // 실제 가입 로직 대신: 가입 후 로그인으로 - navigate("/login"); - }; - - return ( -
-
-
-

회원가입

-
- - - - - - - -
-
-
- ); -} \ No newline at end of file diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/TodoPage.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/TodoPage.jsx" index f91bdde..436d639 100644 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/TodoPage.jsx" +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/TodoPage.jsx" @@ -1,5 +1,62 @@ -import App from '../App'; +// src/pages/TodoPage.jsx +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { fetchTodos, createTodo } from "../api/todoApi"; export default function TodoPage() { - return ; -} \ No newline at end of file + const queryClient = useQueryClient(); + const [title, setTitle] = useState(""); + + const { data: todos, isLoading } = useQuery({ + queryKey: ["todos"], + queryFn: fetchTodos, + }); + + const { mutate: addTodo, isPending } = useMutation({ + mutationFn: createTodo, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["todos"] }); + setTitle(""); + }, + }); + + const handleSubmit = (e) => { + e.preventDefault(); + if (!title.trim()) return; + addTodo(title.trim()); + }; + + if (isLoading) { + return
TODO 불러오는 중...
; + } + + return ( +
+

TODO 리스트

+ +
+ setTitle(e.target.value)} + /> + +
+ +
    + {todos?.map((todo) => ( +
  • + {todo.title} +
  • + ))} +
+
+ ); +} From 8aa5b746934169488cc15242356c1b9395f20f89 Mon Sep 17 00:00:00 2001 From: YousupLim <99yousup@gachon.ac.kr> Date: Tue, 2 Dec 2025 22:28:32 +0900 Subject: [PATCH 4/5] chore: stop tracking .env file --- "Zero100/\354\236\204\354\234\240\354\204\255/my-app/.env" | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/.env" diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/.env" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/.env" deleted file mode 100644 index 55fc0d4..0000000 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/.env" +++ /dev/null @@ -1,2 +0,0 @@ -VITE_API_BASE_URL=https://blog.leets.land -VITE_KAKAO_REDIRECT_PATH=/oauth/kakao \ No newline at end of file From a9dc0c87d2954d251309b3e6743a1f7f6d09d630 Mon Sep 17 00:00:00 2001 From: YousupLim <99yousup@gachon.ac.kr> Date: Wed, 3 Dec 2025 00:10:45 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=2010=EC=A3=BC=EC=B0=A8=20=EA=B3=BC?= =?UTF-8?q?=EC=A0=9C=20=EC=A7=84=ED=96=89=EC=9A=A9=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=ED=8B=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../my-app/.gitignore" | 2 + .../my-app/docs/10week.md" | 2 + .../my-app/src/api/authApi.js" | 79 +++++-------------- .../my-app/src/pages/KakaoCallbackPage.jsx" | 71 ++++++++--------- 4 files changed, 54 insertions(+), 100 deletions(-) create mode 100644 "Zero100/\354\236\204\354\234\240\354\204\255/my-app/docs/10week.md" diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/.gitignore" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/.gitignore" index a547bf3..3b0b403 100644 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/.gitignore" +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/.gitignore" @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/docs/10week.md" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/docs/10week.md" new file mode 100644 index 0000000..649f518 --- /dev/null +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/docs/10week.md" @@ -0,0 +1,2 @@ +10주차 과제 에러 및 공부중 +웹 호스팅+ 환경 변수 설정 진행위한 브랜치 \ No newline at end of file diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/authApi.js" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/authApi.js" index c3a12e5..246c1a7 100644 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/authApi.js" +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/api/authApi.js" @@ -1,68 +1,25 @@ -// src/api/authApi.js -import apiClient, { BASE_URL } from "./client"; +import apiClient from "./client"; -// 🔸 카카오 로그인 버튼에서 사용할 URL (백엔드 → 카카오로 다시 리다이렉트) -export const getKakaoLoginUrl = () => { - return `${BASE_URL}/auth/kakao`; - // Swagger 보고 /auth/kakao 경로가 다르면 여기만 바꿔 주면 됨 -}; - -// 🔸 콜백 페이지에서 호출: /auth/kakao/redirect?code=... export const kakaoRedirect = async (code) => { const res = await apiClient.get("/auth/kakao/redirect", { params: { code }, }); - // 실제 응답 구조는 Swagger에서 확인해서 맞춰야 하지만, - // 기본 형태를 이런 식으로 가정해 둔 거야. - const { accessToken, user } = res.data; - - if (accessToken) { - localStorage.setItem("accessToken", accessToken); - } - if (user) { - localStorage.setItem("user", JSON.stringify(user)); - } - - return user; -}; - -// 🔸 일반 로그인 -export const login = async ({ email, password }) => { - const res = await apiClient.post("/auth/login", { email, password }); - // 실제 경로와 응답 구조를 Swagger에서 확인해서 맞춰줘야 함 - const { accessToken, user } = res.data; - - if (accessToken) { - localStorage.setItem("accessToken", accessToken); - } - if (user) { - localStorage.setItem("user", JSON.stringify(user)); - } - - return user; -}; - -// 🔸 회원가입 -export const signUp = async ({ email, password, nickname }) => { - const res = await apiClient.post("/auth/signup", { - email, - password, - nickname, - }); - - // 필요하면 이쪽에서도 토큰/유저 저장해 줄 수 있음 - return res.data; -}; - -// 🔸 로그아웃 (선택) -export const logout = async () => { - try { - await apiClient.post("/auth/logout"); // 없으면 에러 나도 catch에서 무시 - } catch (e) { - // ignore - } finally { - localStorage.removeItem("accessToken"); - localStorage.removeItem("user"); - } + const result = res.data; + // result 구조: + // { + // code, + // message, + // data: { + // httpStatus, + // responseMessage, + // } + // } + + return { + code: result.code, + message: result.message, + httpStatus: result.data?.httpStatus, + responseMessage: result.data?.responseMessage, + }; }; diff --git "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/KakaoCallbackPage.jsx" "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/KakaoCallbackPage.jsx" index 24633ac..438d3a0 100644 --- "a/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/KakaoCallbackPage.jsx" +++ "b/Zero100/\354\236\204\354\234\240\354\204\255/my-app/src/pages/KakaoCallbackPage.jsx" @@ -1,68 +1,61 @@ // src/pages/KakaoCallbackPage.jsx -import { useEffect } from "react"; + +import React, { useEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; -import { useQuery } from "@tanstack/react-query"; -import { kakaoRedirect } from "../api/authApi"; +import Text from "../components/Text"; +import { kakaoRedirect } from "../api/authApi"; export default function KakaoCallbackPage() { - const location = useLocation(); const navigate = useNavigate(); + const location = useLocation(); - // URL 에서 ?code=... 꺼내기 - const searchParams = new URLSearchParams(location.search); - const code = searchParams.get("code"); - - // 인가 코드가 없으면 바로 에러 문구 - if (!code) { - return
인가 코드가 없습니다. 다시 로그인해 주세요.
; - } + const code = new URLSearchParams(location.search).get("code"); - const { data, error, isLoading, isError } = useQuery({ + const { data, isLoading, isError, error } = useQuery({ queryKey: ["kakaoRedirect", code], queryFn: () => kakaoRedirect(code), - enabled: !!code, - retry: false, + enabled: !!code, // code 있을 때만 호출 }); - // 👉 에러가 났을 때 과제 스펙 처리 useEffect(() => { - if (!isError || !error) return; + if (!data) return; - const status = error?.response?.status; + + if (data.code === 200) { + alert(data.responseMessage || "카카오 로그인 완료"); + navigate("/todo", { replace: true }); + return; + } - // 6-1. 회원가입 필요 -> code 401 - if (status === 401) { + if (data.code === 401) { navigate("/signup?from=kakao", { replace: true }); return; } - // 그 외 상태코드는 일단 콘솔에 찍어두기 - console.log("카카오 리다이렉트 에러", status, error?.response?.data); - }, [isError, error, navigate]); - - // 👉 성공 시 TODO 페이지로 이동 - useEffect(() => { - if (!data) return; - - // 7. 로그인/회원가입 성공 -> code 200 으로 왔다고 가정 - navigate("/todos", { replace: true }); + alert(data.responseMessage || data.message || "카카오 로그인에 실패했습니다."); + navigate("/login", { replace: true }); }, [data, navigate]); if (isLoading) { - return
카카오 로그인 처리 중...
; + return ( +
+ 카카오 로그인 처리 중입니다... +
+ ); } - // 401인 경우에는 위 useEffect에서 /signup 으로 이동하면서 여기 렌더는 거의 안 보일 거고, - // 401이 아닌 다른 에러는 아래 메시지가 보이게 된다. if (isError) { - const status = error?.response?.status; + console.error(error); return ( -
- 로그인 중 오류가 발생했습니다. (status: {status ?? "알 수 없음"}) +
+ 카카오 로그인 중 오류가 발생했습니다.
); } - // data 가 도착하면 위 useEffect에서 /todos 로 이동하므로 여기도 거의 안 보임 - return
카카오 로그인 처리 중...
; -} + return ( +
+ 결과 처리 중입니다... +
+ ); +} \ No newline at end of file