Syw.Frontend

๐Ÿงฉ Vue 2๋กœ ๋ฐฐ์šฐ๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๊ธฐ์ดˆ

1๋‹จ๊ณ„. Vue 2 + Express + Axios ์ธ์ฆ ๊ธฐ๋ฐ˜ ํˆฌ๋‘์•ฑ ๋งŒ๋“ค๊ธฐ

1-13. ๋‹คํฌ๋ชจ๋“œ ์ ์šฉ ๋ฐ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๋กœ UI ์ƒํƒœ ๊ธฐ์–ตํ•˜๊ธฐ

๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ

  • ๋‹คํฌ๋ชจ๋“œ ํ…Œ๋งˆ ํ† ๊ธ€ ๋ฒ„ํŠผ ๋งŒ๋“ค๊ธฐ
  • 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
  1. .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 ์ €์žฅ์†Œ

GitHub - heroyooi/vue2-app at ch13

๐Ÿ’ฌ ๋Œ“๊ธ€

    โ€ป ๋กœ๊ทธ์ธ ํ›„ ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.