๐ฏ ํ์ต ๋ชฉํ
- ๋คํฌ๋ชจ๋ ํ ๋ง ํ ๊ธ ๋ฒํผ ๋ง๋ค๊ธฐ
localStorage
๋ฅผ ํ์ฉํด ๋คํฌ๋ชจ๋ ์ํ ์ ์ฅ/๋ณต์- CSS ํด๋์ค๋ฅผ ์ ์ญ์ผ๋ก ์ ์ดํ๋ ๋ฐฉ๋ฒ ํ์ต
๐๏ธ 1. App.vue์ ํ ๊ธ UI ์ถ๊ฐ
TodoApp ์ปดํฌ๋ํธ๋ ๋ฐ๋ก ๋ค์์ ์ค๋ช ์ด ๋์ต๋๋ค.
<template>
<div :class="['app-container', { dark: isDark }]">
<button class="theme-toggle" @click="toggleDark">
{{ isDark ? 'โ๏ธ ๋ฐ์๋ชจ๋' : '๐ ๋คํฌ๋ชจ๋' }}
</button>
<TodoApp />
</div>
</template>
<script>
import TodoApp from './components/TodoApp.vue';
export default {
components: { TodoApp },
data() {
return {
isDark: false,
};
},
created() {
const saved = localStorage.getItem('theme');
this.isDark = saved === 'dark';
},
methods: {
toggleDark() {
this.isDark = !this.isDark;
localStorage.setItem('theme', this.isDark ? 'dark' : 'light');
},
},
};
</script>
<style scoped>
.app-container {
min-height: 100vh;
padding: 16px;
background-color: #ffffff;
color: #222;
transition: all 0.3s;
}
.app-container.dark {
background-color: #1e1e1e;
color: #f1f1f1;
}
.theme-toggle {
position: absolute;
top: 16px;
right: 16px;
background: none;
border: 1px solid #aaa;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
}
</style>
๐ 2-1. TodoApp.vue ๊ตฌ์กฐ
<template>
<div class="todo-container">
<h2>๐งพ ํ ์ผ ๊ด๋ฆฌ (์์ ํฌํจ)</h2>
<div class="form">
<input v-model="newTodo" placeholder="ํ ์ผ์ ์
๋ ฅํ์ธ์" />
<button @click="addTodo">์ถ๊ฐ</button>
</div>
<ul class="todo-list">
<TodoItem
v-for="todo in todos"
:key="todo.id"
:content="todo.title"
:completed="todo.completed"
:editing="editingId === todo.id"
@delete="removeTodo(todo.id)"
@edit="startEdit(todo.id)"
@update="updateTodo(todo.id, $event)"
@toggle="toggleComplete(todo.id)"
/>
</ul>
</div>
</template>
<script>
import axios from 'axios';
import TodoItem from './TodoItem.vue';
const API_URL = '/api/todos';
export default {
components: { TodoItem },
data() {
return {
newTodo: '',
todos: [],
editingId: null,
userId: null, // userId๋ฅผ ์ํ๋ก ๊ด๋ฆฌ
};
},
mounted() {
this.userId = this.getOrCreateUserId();
this.fetchTodos();
},
methods: {
getOrCreateUserId() {
const KEY = 'todo-user-id';
let id = localStorage.getItem(KEY);
if (!id) {
id = 'user-' + Date.now();
localStorage.setItem(KEY, id);
}
return id;
},
async fetchTodos() {
const res = await axios.get(`${API_URL}?user=${this.userId}`);
this.todos = res.data;
},
async addTodo() {
const title = this.newTodo.trim();
if (!title) return;
const res = await axios.post(API_URL, {
title,
completed: false,
userId: this.userId,
});
this.todos.unshift(res.data);
this.newTodo = '';
},
async removeTodo(id) {
await axios.delete(`${API_URL}/${id}?user=${this.userId}`);
this.todos = this.todos.filter((todo) => todo.id !== id);
},
startEdit(id) {
this.editingId = id;
},
async updateTodo(id, newTitle) {
if (newTitle === null) {
this.editingId = null;
return;
}
const title = newTitle.trim();
if (!title) return;
await axios.put(`${API_URL}/${id}`, { title, userId: this.userId });
this.todos = this.todos.map((todo) =>
todo.id === id ? { ...todo, title } : todo
);
this.editingId = null;
},
async toggleComplete(id) {
const target = this.todos.find((todo) => todo.id === id);
const updated = { ...target, completed: !target.completed };
await axios.put(`${API_URL}/${id}`, {
completed: updated.completed,
userId: this.userId,
});
this.todos = this.todos.map((todo) => (todo.id === id ? updated : todo));
},
},
};
</script>
<!-- ์คํ์ผ ์๋ต -->
๐ฆ SCSS ์ง์ ํจํค์ง ์ค์น (Vite ๊ธฐ์ค)
npm install -D sass sass-loader
.vue
ํ์ผ ๋ด<style lang="scss" scoped>
๋ก ์ ์ธํด์ผ SCSS๊ฐ ์ ์ฉ๋ฉ๋๋ค.
๐ 2-2. TodoApp.vue ์คํ์ผ ์์ฑ
<style lang="scss" scoped>
.todo-container {
max-width: 480px;
margin: 40px auto;
padding: 24px;
border: 1px solid #ccc;
border-radius: 12px;
background: #f9f9f9;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
transition: background-color 0.3s, color 0.3s;
.form {
display: flex;
gap: 8px;
margin-bottom: 16px;
input {
flex: 1;
padding: 8px;
border: 1px solid #aaa;
border-radius: 6px;
font-size: 14px;
background-color: #fff;
color: #222;
transition: background-color 0.3s, color 0.3s;
}
}
button {
padding: 8px 16px;
background-color: #42b983;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #36956d;
}
}
ul {
list-style: none;
padding: 0;
}
}
// ๋คํฌ๋ชจ๋ ๋์
.dark {
.todo-container {
background: #1e1e1e;
color: #f1f1f1;
border-color: #444;
.form {
input {
background-color: #2a2a2a;
color: #f1f1f1;
border-color: #666;
}
}
}
}
</style>
๐ง ํต์ฌ ํฌ์ธํธ ์์ฝ
ํญ๋ชฉ | ์ค๋ช |
---|---|
isDark |
์ปดํฌ๋ํธ ์ํ๋ก ํ ๋ง ํ ๊ธ ์ ์ด |
localStorage |
์๋ก๊ณ ์นจ ํ์๋ ์ฌ์ฉ์์ ํ ๋ง ์ ์ง |
:class="{ dark: isDark }" |
๋คํฌ๋ชจ๋ ํด๋์ค ์ ์ฉ |
scoped CSS |
๊ธฐ๋ณธ ์์ + ๋คํฌ๋ชจ๋ ์์ ์ ์ |
FE ์ ์ฅ์
๐ฌ ๋๊ธ
โป ๋ก๊ทธ์ธ ํ ๋๊ธ์ ์์ฑํ ์ ์์ต๋๋ค.