โ 1. ํด๋ ๊ตฌ์กฐ
src/
โโโ views/
โ โโโ Login.vue
โ โโโ Register.vue
โโโ components/
โ โโโ TodoApp.vue
โโโ App.vue
โโโ main.js
โโโ router.js โ ์๋ก ์ถ๊ฐ
๐ง 2. vue-router ์ค์น ๋ฐ ์ค์
npm install vue-router@3
๐ src/router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Login from './views/Login.vue';
import Register from './views/Register.vue';
import TodoApp from './components/TodoApp.vue';
Vue.use(VueRouter);
const routes = [
{ path: '/login', component: Login },
{ path: '/register', component: Register },
{
path: '/todos',
component: TodoApp,
meta: { requiresAuth: true },
},
{ path: '*', redirect: '/login' },
];
const router = new VueRouter({ mode: 'history', routes });
// โ
์ธ์ฆ ์ฌ๋ถ ์ฒดํฌ
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.matched.some((r) => r.meta.requiresAuth) && !token) {
next('/login');
} else {
next();
}
});
export default router;
๐ 3. main.js
์์ ๋ผ์ฐํฐ ์ฐ๊ฒฐ
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import './assets/base.css';
Vue.config.productionTip = false;
new Vue({
router,
render: (h) => h(App),
}).$mount('#app');
๐ฅ 4. Login.vue
โ ๋ก๊ทธ์ธ ์ปดํฌ๋ํธ
<template>
<div class="auth">
<h2>๋ก๊ทธ์ธ</h2>
<form @submit.prevent="handleLogin">
<input v-model="username" placeholder="ID" />
<input v-model="password" type="password" placeholder="Password" />
<button type="submit">๋ก๊ทธ์ธ</button>
</form>
<router-link to="/register">ํ์๊ฐ์
</router-link>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'LoginPage',
data() {
return { username: '', password: '' };
},
methods: {
async handleLogin() {
try {
const res = await axios.post('/api/auth/login', {
username: this.username,
password: this.password,
});
localStorage.setItem('token', res.data.token);
localStorage.setItem('username', res.data.username);
this.$router.push('/todos');
} catch (e) {
alert('๋ก๊ทธ์ธ ์คํจ');
}
},
},
};
</script>
๐ฅ 5. Register.vue
โ ํ์๊ฐ์
์ปดํฌ๋ํธ
<template>
<div class="auth">
<h2>ํ์๊ฐ์
</h2>
<form @submit.prevent="handleRegister">
<input v-model="username" placeholder="ID" />
<input v-model="password" type="password" placeholder="Password" />
<button type="submit">ํ์๊ฐ์
</button>
</form>
<router-link to="/login">๋ก๊ทธ์ธ</router-link>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'RegisterPage',
data() {
return { username: '', password: '' };
},
methods: {
async handleRegister() {
try {
await axios.post('/api/auth/register', {
username: this.username,
password: this.password,
});
alert('ํ์๊ฐ์
์๋ฃ! ๋ก๊ทธ์ธ ํด์ฃผ์ธ์.');
this.$router.push('/login');
} catch (e) {
alert('ํ์๊ฐ์
์คํจ');
}
},
},
};
</script>
๐ 6. Axios์ ํ ํฐ ์๋ ์ถ๊ฐ
๐ main.js
import axios from 'axios';
axios.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
โ
7. /todos
์ ๊ทผ ์ ์ด ํ์ธ
- ๋ก๊ทธ์ธํ์ง ์์ ์ํ๋ก
/todos
์ ๊ทผ ์ ์๋์ผ๋ก/login
์ผ๋ก ์ด๋ - ๋ก๊ทธ์ธ ์ฑ๊ณต ํ
/todos
์ ์ ์ ๊ทผ ๊ฐ๋ฅ - ์๋ก๊ณ ์นจํด๋ ์ธ์ฆ ์ ์ง๋จ (๋ก์ปฌ์คํ ๋ฆฌ์ง ๊ธฐ๋ฐ)
โ
8. App.vue
(๋ค๋น๊ฒ์ด์
+ ๋คํฌ๋ชจ๋ + ๋ผ์ฐํฐ๋ทฐ + ๋ก๊ทธ์์)
<template>
<div :class="['app-container', { dark: isDark }]">
<header class="nav-bar">
<div class="left">
<router-link to="/todos">ํ ์ผ</router-link>
<router-link to="/login">๋ก๊ทธ์ธ</router-link>
<router-link to="/register">ํ์๊ฐ์
</router-link>
</div>
<div class="right">
<button @click="toggleDark">
{{ isDark ? 'โ๏ธ ๋ฐ์๋ชจ๋' : '๐ ๋คํฌ๋ชจ๋' }}
</button>
<button v-if="isLoggedIn" @click="logout">๐ ๋ก๊ทธ์์</button>
</div>
</header>
<!-- ๋ผ์ฐํ
์ ๋ฐ๋ผ ํ์ด์ง ๋ถ๊ธฐ -->
<router-view />
</div>
</template>
<script>
export default {
data() {
return {
isDark: false,
isLoggedIn: !!localStorage.getItem('token'),
};
},
created() {
const saved = localStorage.getItem('theme');
this.isDark = saved === 'dark';
},
methods: {
toggleDark() {
this.isDark = !this.isDark;
localStorage.setItem('theme', this.isDark ? 'dark' : 'light');
},
logout() {
localStorage.removeItem('token');
this.isLoggedIn = false;
this.$router.push('/login');
},
},
watch: {
// ๋ก๊ทธ์ธ ์ฑ๊ณต ์์๋ ๊ฐฑ์ ๋ ์ ์๋๋ก localStorage ๊ฐ์
$route() {
this.isLoggedIn = !!localStorage.getItem('token');
},
},
};
</script>
<style lang="scss" scoped>
.app-container {
min-height: 100vh;
background-color: #fff;
color: #222;
transition: all 0.3s;
&.dark {
background-color: #1e1e1e;
color: #f1f1f1;
a {
color: #f1f1f1;
}
}
}
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #ddd;
background: #f5f5f5;
.left a {
margin-right: 16px;
text-decoration: none;
color: #333;
}
.right button {
margin-left: 8px;
padding: 6px 12px;
border-radius: 4px;
border: 1px solid #aaa;
cursor: pointer;
background: none;
}
}
.dark .nav-bar {
background: #2a2a2a;
border-color: #444;
.left a {
color: #f1f1f1;
}
.right button {
color: #f1f1f1;
border-color: #666;
}
}
</style>
BE ์ ์ฅ์
๐ฌ ๋๊ธ
โป ๋ก๊ทธ์ธ ํ ๋๊ธ์ ์์ฑํ ์ ์์ต๋๋ค.