그랜파 개발자의 프론트엔드 공부-Vue

Vue로 PWA 개발, ChatGPT의 프론트엔드 앱 CRUD 예제의 소스 코드

그랜파 개발자 2024. 12. 3. 03:53

Vue로 PWA 개발 - 그랜파 개발자.

 

ChatGPT의 PWA 예제의 소스 코드로 ChatGPT의 Router 예제와 PWA 예제를 병합하였습니다.
예제 소스 코드의 경우 ChatGPT가 제공한 코드와 직접 코딩한 코드에 차이가 있을 수 있습니다. 실행하는 과정에서 맞지 않는 코드들은 수정하여 실행하도록 하였습니다.


프로젝트가 진행되면 소스 코드는 계속 추가될 것입니다.

src/firebase.js

// src/firebase.js
import { initializeApp } from "firebase/app";
import { getFirestore, collection, getDocs, query } from "firebase/firestore";

const firebaseConfig = {
  apiKey: process.env.VUE_APP_FIREBASE_API_KEY,
  authDomain: process.env.VUE_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.VUE_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.VUE_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.VUE_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.VUE_APP_FIREBASE_APP_ID,
};

// npm install dotenv - env가 정상 동작하지 않을 때 설치 필요함

// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Initialize Firestore 
const db = getFirestore(app);

export { db, collection, getDocs, query };

src/router/index.js

// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue';
import PostList from '@/views/PostList.vue';
import About from '@/views/About.vue';
import Contact from '@/views/Contact.vue';

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/post',
    name: 'Post',
    component: PostList,
  },
  {
    path: '/about',
    name: 'About',
    component: About,
  },
  {
    path: '/contact',
    name: 'Contact',
    component: Contact,
  },
];

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

src/store/index.js

// src/store/index.js
import Vue from "vue";
import Vuex from "vuex";
import { db } from "@/firebase";
import { collection, query, getDocs, addDoc, updateDoc, doc } from "firebase/firestore";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    posts: []
  },
  mutations: {
    setPosts(state, posts) {
      state.posts = posts;
    },
    addPost(state, post) {
      state.posts.push(post);
    },
    updatePost(state, updatedPost) {
      const index = state.posts.findIndex(post => post.id === updatedPost.id);
      if (index !== -1) {
        Vue.set(state.posts, index, updatedPost);
      }
    }
  },
  actions: {
    async fetchPosts({ commit }) {
      const querySnapshot = await getDocs(collection(db, "posts"));
      const posts = querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
      commit("setPosts", posts);
    },
    async addPost({ commit }, post) {
      const docRef = await addDoc(collection(db, "posts"), post);
      commit("addPost", { id: docRef.id, ...post });
    },
    async updatePost({ commit }, { id, post }) {
      const docRef = doc(db, "posts", id);
      await updateDoc(docRef, post);
      commit("updatePost", { id, ...post });
    }
  },
  getters: {
    allPosts: state => state.posts
  }
});

src/App.vue

<!-- src/App.vue -->
<template>
  <v-app>
    <!-- App Bar -->
    <v-app-bar color="primary" dark app>
      <v-app-bar-nav-icon @click="toggleDrawer" />
      <v-toolbar-title>My App</v-toolbar-title>
    </v-app-bar>

    <!-- Navigation Drawer -->
    <v-navigation-drawer v-model="drawer" app>
      <v-list>
        <v-list-item-group>
          <v-list-item
            v-for="(item, index) in menuItems"
            :key="index"
            @click="navigateTo(item)"
          >
            <v-list-item-icon>
              <v-icon>{{ item.icon }}</v-icon>
            </v-list-item-icon>
            <v-list-item-content>
              <v-list-item-title>{{ item.title }}</v-list-item-title>
            </v-list-item-content>
          </v-list-item>
        </v-list-item-group>
      </v-list>
    </v-navigation-drawer>

    <!-- Main Content -->
    <v-main>
      <v-container>
        <router-view /> <!-- 라우터가 여기에 컴포넌트를 렌더링 -->
      </v-container>
    </v-main>

    <!-- Footer -->
    <v-footer app color="secondary" dark>
      <v-container>
        <p>&copy; {{ new Date().getFullYear() }} My App. All rights reserved.</p>
      </v-container>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      drawer: false,
      menuItems: [
        { title: 'Home', icon: 'mdi-home', path: '/' },
        { title: 'Post', icon: 'mdi-pencil', path: '/post' },
        { title: 'About', icon: 'mdi-information', path: '/about' },
        { title: 'Contact', icon: 'mdi-phone', path: '/contact' },
      ],
    };
  },
  methods: {
    toggleDrawer() {
      this.drawer = !this.drawer;
    },
    navigateTo(item) {
      this.drawer = false; // 메뉴 클릭 시 Drawer 닫기
      // 같은 페이지를 라우팅하면 에러가 발생한다.
      if(this.$route.name !== item.title )    
        this.$router.push(item.path);
    },
  },
};
</script>

src/views/PostList.vue

<!-- src/views/PostList.vue -->
<template>
  <v-container>
    <v-btn @click="openDialog">글 작성하기</v-btn>

    <v-dialog v-model="dialog" max-width="500px">
      <v-card>
        <v-card-title>새 글 작성</v-card-title>
        <v-card-text>
          <v-text-field label="제목" v-model="newPost.title"></v-text-field>
          <v-textarea label="내용" v-model="newPost.content"></v-textarea>
        </v-card-text>
        <v-card-actions>
          <v-btn color="primary" @click="savePost">저장</v-btn>
          <v-btn color="secondary" @click="closeDialog">취소</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <v-list>
      <v-list-item v-for="post in posts" :key="post.id" @click="editPost(post)">
        <v-list-item-content>
          <v-list-item-title>{{ post.title }}</v-list-item-title>
          <v-list-item-subtitle>{{ post.content }}</v-list-item-subtitle>
        </v-list-item-content>
      </v-list-item>
    </v-list>
  </v-container>
</template>

<script>
import { mapActions, mapGetters } from "vuex";

export default {
  data() {
    return {
      dialog: false,
      newPost: {
        title: "",
        content: ""
      }
    };
  },
  computed: {
    ...mapGetters(["allPosts"]),
    posts() {
      return this.allPosts;
    }
  },
  methods: {
    ...mapActions(["fetchPosts", "addPost", "updatePost"]),
    openDialog() {
      this.newPost = { title: "", content: "" };
      this.dialog = true;
    },
    closeDialog() {
      this.dialog = false;
    },
    savePost() {
      if (this.newPost.id) {
        this.updatePost({ id: this.newPost.id, post: this.newPost });
      } else {
        this.addPost(this.newPost);
      }
      this.closeDialog();
    },
    editPost(post) {
      this.newPost = { ...post };
      this.dialog = true;
    }
  },
  created() {
    this.fetchPosts();
  }
};
</script>

 

Vue 프로젝트 Beta Test : mylog, 일상의 기록