diff --git a/package-lock.json b/package-lock.json index 1bc147c..ea5c53c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.0.0", "dependencies": { "axios": "^1.6.8", + "jwt-decode": "^4.0.0", "pinia": "^2.1.7", + "pinia-plugin-persistedstate": "^3.2.1", "vue": "^3.4.21", "vue-router": "^4.3.0" }, @@ -2847,6 +2849,14 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3239,6 +3249,14 @@ } } }, + "node_modules/pinia-plugin-persistedstate": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz", + "integrity": "sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==", + "peerDependencies": { + "pinia": "^2.0.0" + } + }, "node_modules/pinia/node_modules/vue-demi": { "version": "0.14.7", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", diff --git a/package.json b/package.json index e726799..3d73068 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ }, "dependencies": { "axios": "^1.6.8", + "jwt-decode": "^4.0.0", "pinia": "^2.1.7", + "pinia-plugin-persistedstate": "^3.2.1", "vue": "^3.4.21", "vue-router": "^4.3.0" }, diff --git a/src/api/article.js b/src/api/article.js index a566dfa..a876094 100644 --- a/src/api/article.js +++ b/src/api/article.js @@ -2,9 +2,9 @@ import { localAxios } from '@/utils/http-commons'; const local = localAxios; -function getArticles(success, fail) { +function getArticles(params,success, fail) { //list - local.get('/article/list').then(success).catch(fail); + local.get('/article/list',{ params }).then(success).catch(fail); } function searchArticle(keyword, success, fail) { diff --git a/src/api/member.js b/src/api/member.js new file mode 100644 index 0000000..a58531a --- /dev/null +++ b/src/api/member.js @@ -0,0 +1,27 @@ +import { localAxios } from "@/utils/http-commons"; + +const local = localAxios; + +async function userConfirm(param, success, fail) { + await local.post(`/member/login`, param).then(success).catch(fail); +} + +async function findById(userid, success, fail) { + local.defaults.headers["Authorization"] = sessionStorage.getItem("accessToken"); + await local.get(`/member/info/${userid}`).then(success).catch(fail); +} + +async function tokenRegeneration(user, success, fail) { + local.defaults.headers["refreshToken"] = sessionStorage.getItem("refreshToken"); //axios header에 refresh-token 셋팅 + await local.post(`/member/refresh`, user).then(success).catch(fail); +} + +async function logout(userid, success, fail) { + await local.get(`/member/logout/${userid}`).then(success).catch(fail); +} + +function registMember(member, success, fail) { + local.post('/member/join',JSON.stringify(member)).then(success).catch(fail); +} + +export { userConfirm, findById, tokenRegeneration, logout, registMember }; diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 9cb2404..142119c 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -1,5 +1,21 @@ @@ -11,11 +27,31 @@ import { RouterLink } from 'vue-router'; 검색 게시판 - 로그인 + + + + + + {{ + menu.name + }} + + + + + {{ + menu.name + }} + + + + + + diff --git a/src/components/article/ArticleList.vue b/src/components/article/ArticleList.vue index fa74bbe..6e69890 100644 --- a/src/components/article/ArticleList.vue +++ b/src/components/article/ArticleList.vue @@ -1,12 +1,43 @@ @@ -30,6 +61,7 @@ getArticles(({ data }) => (articles.value = data)); + diff --git a/src/components/users/UserMyPage.vue b/src/components/users/UserMyPage.vue new file mode 100644 index 0000000..1dc9894 --- /dev/null +++ b/src/components/users/UserMyPage.vue @@ -0,0 +1,40 @@ + + + + + + + + 내정보 + + + + + + + + + + + + + + + + + + + 수정 + + + + + + + diff --git a/src/components/users/UserRegister.vue b/src/components/users/UserRegister.vue new file mode 100644 index 0000000..28b9e7c --- /dev/null +++ b/src/components/users/UserRegister.vue @@ -0,0 +1,82 @@ + + + + + + + + 회원가입 + + + + + + 이름 : + + + + 아이디 : + + + + 비밀번호 : + + + + 비밀번호확인 : + + + + 이메일 : + + + @ + + 선택 + 싸피 + 구글 + 네이버 + 카카오 + + + + + 회원가입 + 초기화 + + + + + + + + diff --git a/src/main.js b/src/main.js index 841d69f..4a916ea 100644 --- a/src/main.js +++ b/src/main.js @@ -2,13 +2,20 @@ import './assets/main.css'; import { createApp } from 'vue'; import { createPinia } from 'pinia'; +import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; import App from './App.vue'; import router from './router'; const app = createApp(App); +const pinia = createPinia(); + +pinia.use(piniaPluginPersistedstate); app.use(createPinia()); app.use(router); -app.mount('#app'); +// app.mount('#app'); +router.isReady().then(() => { + app.mount("#app"); + }); diff --git a/src/router/index.js b/src/router/index.js index f8903b4..0c6205f 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,6 +1,27 @@ import { createRouter, createWebHistory } from 'vue-router'; import HomeView from '@/views/HomeView.vue'; +import { storeToRefs } from "pinia"; + +import { useMemberStore } from "@/stores/member"; + +const onlyAuthUser = async (to, from, next) => { + const memberStore = useMemberStore(); + const { userInfo, isValidToken } = storeToRefs(memberStore); + const { getUserInfo } = memberStore; + + let token = sessionStorage.getItem("accessToken"); + + if (userInfo.value != null && token) { + await getUserInfo(token); + } + if (!isValidToken.value || userInfo.value === null) { + next({ name: "user-login" }); + } else { + next(); + } +}; + const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ @@ -9,6 +30,34 @@ const router = createRouter({ name: 'home', component: HomeView, }, + { + path: "/user", + name: "user", + component: () => import("@/views/TheUserView.vue"), + children: [ + { + path: "login", + name: "user-login", + component: () => import("@/components/users/UserLogin.vue"), + }, + { + path: "join", + name: "user-join", + component: () => import("@/components/users/UserRegister.vue"), + }, + { + path: "mypage", + name: "user-mypage", + beforeEnter: onlyAuthUser, + component: () => import("@/components/users/UserMyPage.vue"), + }, + // { + // path: "modify/:userid", + // name: "user-modify", + // component: () => import("@/components/users/UserModify.vue"), + // }, + ], + }, { path: '/board', component: () => import('@/views/BoardView.vue'), diff --git a/src/stores/member.js b/src/stores/member.js new file mode 100644 index 0000000..05e73a0 --- /dev/null +++ b/src/stores/member.js @@ -0,0 +1,139 @@ +import { ref } from "vue" +import { useRouter } from "vue-router" +import { defineStore } from "pinia" +import { jwtDecode } from "jwt-decode" + +import { userConfirm, findById, tokenRegeneration, logout } from "@/api/member" +import { httpStatusCode } from "@/utils/http-status" + +export const useMemberStore = defineStore("memberStore", () => { + const router = useRouter() + + const isLogin = ref(false) + const isLoginError = ref(false) + const userInfo = ref(null) + const isValidToken = ref(false) + + const userLogin = async (loginUser) => { + await userConfirm( + loginUser, + (response) => { + if (response.status === httpStatusCode.CREATE) { + console.log("로그인 성공!!!!") + let { data } = response + let accessToken = data["access-token"] + let refreshToken = data["refresh-token"] + isLogin.value = true + isLoginError.value = false + isValidToken.value = true + sessionStorage.setItem("accessToken", accessToken) + sessionStorage.setItem("refreshToken", refreshToken) + } + }, + (error) => { + console.log("로그인 실패!!!!") + isLogin.value = false + isLoginError.value = true + isValidToken.value = false + console.error(error) + } + ) + } + + const getUserInfo = async (token) => { + let decodeToken = jwtDecode(token) + console.log(decodeToken) + await findById( + decodeToken.userId, + (response) => { + if (response.status === httpStatusCode.OK) { + userInfo.value = response.data.userInfo + } else { + console.log("유저 정보 없음!!!!") + } + }, + async (error) => { + console.error( + "g[토큰 만료되어 사용 불가능.] : ", + error.response.status, + error.response.statusText + ) + isValidToken.value = false + + await tokenRegenerate() + } + ) + } + + const tokenRegenerate = async () => { + await tokenRegeneration( + JSON.stringify(userInfo.value), + (response) => { + if (response.status === httpStatusCode.CREATE) { + let accessToken = response.data["access-token"] + sessionStorage.setItem("accessToken", accessToken) + isValidToken.value = true + } + }, + async (error) => { + // HttpStatus.UNAUTHORIZE(401) : RefreshToken 기간 만료 >> 다시 로그인!!!! + if (error.response.status === httpStatusCode.UNAUTHORIZED) { + // 다시 로그인 전 DB에 저장된 RefreshToken 제거. + await logout( + userInfo.value.userid, + (response) => { + if (response.status === httpStatusCode.OK) { + console.log("리프레시 토큰 제거 성공") + } else { + console.log("리프레시 토큰 제거 실패") + } + alert("RefreshToken 기간 만료!!! 다시 로그인해 주세요.") + isLogin.value = false + userInfo.value = null + isValidToken.value = false + router.push({ name: "user-login" }) + }, + (error) => { + console.error(error) + isLogin.value = false + userInfo.value = null + } + ) + } + } + ) + } + + const userLogout = async () => { + console.log("로그아웃 아이디 : " + userInfo.value.userId) + await logout( + userInfo.value.userId, + (response) => { + if (response.status === httpStatusCode.OK) { + isLogin.value = false + userInfo.value = null + isValidToken.value = false + + sessionStorage.removeItem("accessToken") + sessionStorage.removeItem("refreshToken") + } else { + console.error("유저 정보 없음!!!!") + } + }, + (error) => { + console.log(error) + } + ) + } + + return { + isLogin, + isLoginError, + userInfo, + isValidToken, + userLogin, + getUserInfo, + tokenRegenerate, + userLogout, + } +}) diff --git a/src/stores/menu.js b/src/stores/menu.js new file mode 100644 index 0000000..83d9bbb --- /dev/null +++ b/src/stores/menu.js @@ -0,0 +1,19 @@ +import { ref } from "vue"; +import { defineStore } from "pinia"; + +export const useMenuStore = defineStore("menuStore", () => { + const menuList = ref([ + { name: "로그인", show: true, routeName: "user-login" }, + { name: "회원가입", show: true, routeName: "user-join" }, + { name: "내정보", show: false, routeName: "user-mypage" }, + { name: "로그아웃", show: false, routeName: "user-logout" }, + ]); + + const changeMenuState = () => { + menuList.value = menuList.value.map((item) => ({ ...item, show: !item.show })); + }; + return { + menuList, + changeMenuState, + }; +}); diff --git a/src/utils/http-status.js b/src/utils/http-status.js new file mode 100644 index 0000000..76ecca4 --- /dev/null +++ b/src/utils/http-status.js @@ -0,0 +1,11 @@ +// HTTP Status Code +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status +export const httpStatusCode = { + OK: 200, + CREATE: 201, + NOCONTENT: 204, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOTFOUND: 404, + }; + \ No newline at end of file diff --git a/src/views/TheUserView.vue b/src/views/TheUserView.vue new file mode 100644 index 0000000..4681a42 --- /dev/null +++ b/src/views/TheUserView.vue @@ -0,0 +1,14 @@ + + + + + Member Service + + + + +