feat : page upload

This commit is contained in:
leejunghyeok 2024-05-20 08:44:37 +09:00
parent 392e846342
commit 361287b098
15 changed files with 565 additions and 7 deletions

18
package-lock.json generated
View File

@ -9,7 +9,9 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"axios": "^1.6.8", "axios": "^1.6.8",
"jwt-decode": "^4.0.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
@ -2847,6 +2849,14 @@
"graceful-fs": "^4.1.6" "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": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "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": { "node_modules/pinia/node_modules/vue-demi": {
"version": "0.14.7", "version": "0.14.7",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz",

View File

@ -12,7 +12,9 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.6.8", "axios": "^1.6.8",
"jwt-decode": "^4.0.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },

View File

@ -2,9 +2,9 @@ import { localAxios } from '@/utils/http-commons';
const local = localAxios; const local = localAxios;
function getArticles(success, fail) { function getArticles(params,success, fail) {
//list //list
local.get('/article/list').then(success).catch(fail); local.get('/article/list',{ params }).then(success).catch(fail);
} }
function searchArticle(keyword, success, fail) { function searchArticle(keyword, success, fail) {

27
src/api/member.js Normal file
View File

@ -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 };

View File

@ -1,5 +1,21 @@
<script setup> <script setup>
import { RouterLink } from 'vue-router'; import { RouterLink } from 'vue-router';
import { useMenuStore } from "@/stores/menu";
import { useMemberStore } from "@/stores/member";
import { storeToRefs } from "pinia";
const menuStore = useMenuStore();
const memberStore = useMemberStore();
const { menuList } = storeToRefs(menuStore);
const { changeMenuState } = menuStore;
const { userLogout } = memberStore;
const logout = () => {
userLogout();
changeMenuState();
};
</script> </script>
<template> <template>
@ -11,11 +27,31 @@ import { RouterLink } from 'vue-router';
<div class="menuWrapper"> <div class="menuWrapper">
<div class="item"><RouterLink :to="{ name: 'home' }">검색</RouterLink></div> <div class="item"><RouterLink :to="{ name: 'home' }">검색</RouterLink></div>
<div class="item"><RouterLink :to="{ name: 'board' }">게시판</RouterLink></div> <div class="item"><RouterLink :to="{ name: 'board' }">게시판</RouterLink></div>
<div class="item" v-if="true"><RouterLink :to="{ name: 'home' }">로그인</RouterLink></div>
<template v-for="menu in menuList" :key="menu.routeName">
<template v-if="menu.show">
<template v-if="menu.routeName === 'user-logout'">
<div class="item">
<router-link to="/" @click.prevent="logout" class="nav-link">{{
menu.name
}}</router-link>
</div>
</template>
<template v-else>
<div class="item">
<router-link :to="{ name: menu.routeName }" class="nav-link">{{
menu.name
}}</router-link>
</div>
</template>
</template>
</template>
<!-- <div class="item" v-if="true"><RouterLink :to="{ name: 'user-login' }">로그인</RouterLink></div>
<template v-else> <template v-else>
<div class="item"><RouterLink :to="{ name: 'home' }">마이페이지</RouterLink></div> <div class="item"><RouterLink :to="{ name: 'home' }">마이페이지</RouterLink></div>
<div class="item"><RouterLink :to="{ name: 'home' }">로그아웃</RouterLink></div> <div class="item"><RouterLink :to="{ name: 'home' }">로그아웃</RouterLink></div>
</template> </template> -->
</div> </div>
</nav> </nav>
</header> </header>

View File

@ -1,12 +1,43 @@
<script setup> <script setup>
import { getArticles } from '@/api/article'; import { getArticles } from '@/api/article';
import { ref } from 'vue'; import { ref , reactive } from 'vue';
import { RouterLink } from 'vue-router'; import { RouterLink } from 'vue-router';
import FilledButton from '../common/FilledButton.vue'; import FilledButton from '../common/FilledButton.vue';
import PageNavigation from "../common/PageNavigation.vue";
const articles = ref([]); const articles = ref([]);
getArticles(({ data }) => (articles.value = data)); const params = reactive({
pageNo: 1,
key: 'all',
word: ''
})
const currentPage = ref(1)
const totalpage = ref(1)
function searchList() {
getArticles(
params,
({ data }) => {
articles.value = data.articles
currentPage.value = data.page.pageNo
totalpage.value = data.page.total
console.log(articles.value)
console.log(currentPage.value)
console.log(totalpage.value)
},
(error) => {
console.log(error)
}
);
}
searchList()
function pageChange(value) {
params.pageNo = value
searchList()
}
</script> </script>
<template> <template>
@ -30,6 +61,7 @@ getArticles(({ data }) => (articles.value = data));
</RouterLink> </RouterLink>
</li> </li>
</ul> </ul>
<PageNavigation :currentPage="currentPage" :totalPage="totalpage" @page-change="pageChange" />
</template> </template>
<style scoped> <style scoped>

View File

@ -0,0 +1,82 @@
<script setup>
import { ref } from "vue"
import { storeToRefs } from "pinia"
import { useRouter } from "vue-router"
import { useMemberStore } from "@/stores/member"
import { useMenuStore } from "@/stores/menu"
const router = useRouter()
const memberStore = useMemberStore()
const { isLogin, isLoginError } = storeToRefs(memberStore)
const { userLogin, getUserInfo } = memberStore
const { changeMenuState } = useMenuStore()
const loginUser = ref({
id: "",
password: "",
})
const login = async () => {
await userLogin(loginUser.value)
let token = sessionStorage.getItem("accessToken")
console.log(token)
console.log("isLogin: " + isLogin.value)
if (isLogin.value) {
getUserInfo(token)
changeMenuState()
router.replace("/")
}
}
</script>
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<h2 class="my-3 py-3 shadow-sm bg-light text-center">
<mark class="orange">로그인</mark>
</h2>
</div>
<div class="col-lg-10">
<form>
<div class="form-check mb-3 float-end">
<input class="form-check-input" type="checkbox" />
<label class="form-check-label" for="saveid"> 아이디저장 </label>
</div>
<div class="mb-3 text-start">
<label for="userid" class="form-label">아이디 : </label>
<input
type="text"
class="form-control"
v-model="loginUser.id"
placeholder="아이디..."
/>
</div>
<div class="mb-3 text-start">
<label for="userpwd" class="form-label">비밀번호 : </label>
<input
type="password"
class="form-control"
v-model="loginUser.password"
@keyup.enter="login"
placeholder="비밀번호..."
/>
</div>
<div class="mb-3 text-start" v-if="isLoginError === true">
<div class="alert alert-danger" role="alert">아이디 또는 비밀번호 확인해 주세요</div>
</div>
<div class="col-auto text-center">
<button type="button" class="btn btn-outline-primary mb-3" @click="login">
로그인
</button>
<button type="button" class="btn btn-outline-success ms-1 mb-3">회원가입</button>
</div>
</form>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,40 @@
<script setup></script>
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<h2 class="my-3 py-3 shadow-sm bg-light text-center">
<mark class="orange">내정보</mark>
</h2>
</div>
<div class="col-lg-10">
<div class="card mt-3 m-auto" style="max-width: 700px">
<div class="row g-0">
<div class="col-md-4">
<!-- <img
src="https://source.unsplash.com/random/250x250/?food"
class="img-fluid rounded-start"
alt="..."
/> -->
</div>
<div class="col-md-8">
<div class="card-body text-start">
<ul class="list-group list-group-flush">
<!-- <li class="list-group-item">SSAFY</li>
<li class="list-group-item">김싸피</li>
<li class="list-group-item">ssafy@ssafy.com</li> -->
</ul>
</div>
</div>
</div>
</div>
<div>
<button type="button" class="btn btn-outline-secondary mt-2">수정</button>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,82 @@
<script setup>
import { ref } from "vue";
import { registMember } from "@/api/member";
import { useRouter } from "vue-router";
const router = useRouter();
const email_id = ref("")
const email_domain = ref("")
const member = ref({
id: "",
name: "",
password: "",
email: email_id.value + "@" + email_domain.value,
})
function onSubmit() {
member.value.email = email_id.value + "@" + email_domain.value
console.log(member.value)
registMember(
member.value,
(response) => {
if (response.status == 200) console.log("회원가입 성공!")
router.push({ name : "user-login"})
},
(error) => console.error(error)
)
}
</script>
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<h2 class="my-3 py-3 shadow-sm bg-light text-center">
<mark class="orange">회원가입</mark>
</h2>
</div>
<div class="col-lg-10 text-start">
<form>
<div class="mb-3">
<label for="username" class="form-label">이름 : </label>
<input type="text" class="form-control" placeholder="이름..." v-model="member.name"/>
</div>
<div class="mb-3">
<label for="userid" class="form-label">아이디 : </label>
<input type="text" class="form-control" placeholder="아이디..." v-model="member.id"/>
</div>
<div class="mb-3">
<label for="userpwd" class="form-label">비밀번호 : </label>
<input type="text" class="form-control" placeholder="비밀번호..." v-model="member.password"/>
</div>
<div class="mb-3">
<label for="pwdcheck" class="form-label">비밀번호확인 : </label>
<input type="text" class="form-control" id="pwdcheck" placeholder="비밀번호확인..." />
</div>
<div class="mb-3">
<label for="emailid" class="form-label">이메일 : </label>
<div class="input-group">
<input type="text" class="form-control" placeholder="이메일아이디" v-model="email_id"/>
<span class="input-group-text">@</span>
<select class="form-select" aria-label="이메일 도메인 선택" v-model="email_domain">
<option selected>선택</option>
<option value="ssafy.com">싸피</option>
<option value="google.com">구글</option>
<option value="naver.com">네이버</option>
<option value="kakao.com">카카오</option>
</select>
</div>
</div>
<div class="col-auto text-center">
<button type="button" class="btn btn-outline-primary mb-3" @click="onSubmit">회원가입</button>
<button type="button" class="btn btn-outline-success ms-1 mb-3">초기화</button>
</div>
</form>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@ -2,13 +2,20 @@ import './assets/main.css';
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
const app = createApp(App); const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(createPinia()); app.use(createPinia());
app.use(router); app.use(router);
app.mount('#app'); // app.mount('#app');
router.isReady().then(() => {
app.mount("#app");
});

View File

@ -1,6 +1,27 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue'; 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({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
@ -9,6 +30,34 @@ const router = createRouter({
name: 'home', name: 'home',
component: HomeView, 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', path: '/board',
component: () => import('@/views/BoardView.vue'), component: () => import('@/views/BoardView.vue'),

139
src/stores/member.js Normal file
View File

@ -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,
}
})

19
src/stores/menu.js Normal file
View File

@ -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,
};
});

11
src/utils/http-status.js Normal file
View File

@ -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,
};

14
src/views/TheUserView.vue Normal file
View File

@ -0,0 +1,14 @@
<script setup></script>
<template>
<div class="container text-center mt-3">
<div class="alert alert-primary" role="alert">Member Service</div>
<router-view></router-view>
</div>
</template>
<style>
mark.orange {
background: linear-gradient(to top, rgb(243, 164, 18) 20%, transparent 30%);
}
</style>