TIL - 24주차 코드

 

1. Vue.js 인증과 권한 : 230508

1) Pinia 사용 방법

  • pinia 설치 방법** : npm install pinia

a. Pinia 기본 설정

  • main.js
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import { createPinia } from 'pinia'


  const pinia = createPinia();

  // Vue.createApp이 글로벌 라이브러리이다.
createApp(App)
.use(router)
.use(pinia)
.mount('#app') // -> ES6 라이브러리이다.


  • useUserDetailsStore.js
    • ‘export const’로 모듈을 설정하면, 옵션s API로 기능이 사용된다. 즉, 다른 컴포넌트에서 해당 전역 변수를 사용시, 함수인 ()로 불러내어 사용해야 한다.**
    • export default로 쓰면, ()를 쓰면 안 된다! useUserDetailsStore라는 변수도 지우자
    • 하지만, export const가 pinia 매뉴얼 대로 기본이다!
import {defineStore} from "pinia";

// export const로 하면, 옵션s API로 기능이 사용된다. 즉, 함수인 ()로 사용해야 한다.
// export default로 쓰면, ()를 쓰면 안 된다! 변수도 지우자
// 하지만 export const가 기본이다!
export const useUserDetailsStore = defineStore("userDetails", {
    state:()=>({
        id:0,
        username:null,
        email:null,
        role:null
    })
});



2) 로그인 기능에서 Pinia 사용 방법

a. Pinia 사용 방법 : 상태관리 전역 변수

  • import {useUserDetailsStore} from '../stores/useUserDetailsStore';

  • import된 pinia 상태 관리 변수를 함수로 만들어서 사용한다.

  • 여기서 ()가 ‘state:()=>{}’ 를 의미한다.

  • 인증된 정보를 저장하기 위해서 SSR에서는 Session이나 쿠키를 사용했는데

  • CSR에서는 session이나 쿠키가 필요없다. 페이지를 갱신하지 않으면 전역변수가 존재해서 편리하다.



  • ‘export const’로 모듈을 설정하면, 옵션s API로 기능이 사용된다. 즉, 다른 컴포넌트에서 해당 전역 변수를 사용시, 함수인 ()로 불러내어 사용해야 한다.**

  • export default로 쓰면, ()를 쓰면 안 된다! useUserDetailsStore라는 변수도 지우자

  • 하지만, export const가 pinia 매뉴얼 대로 기본이다!




b. 실습 코드 :


  • Login.vue
<script setup>
import {reactive} from 'vue';
import { useRouter } from 'vue-router';

// 전역 변수를 선언! store에서 가져온다.
// 하나의 모듈에다가 만들어서 공유하면 전역변수가 된다. 
// 모두다 같은 객체를 공유하는지가 궁금하다! 그래서, 여기서 담아서 다른페이지에서 읽어보자!!
// import userDetails from '../stores/UserDetails.js';


let userDetails = useUserDetailsStore();

let router = useRouter();

let user = reactive({
  username:"newlec",
  password:"111",
  role:""
});

async function logHandler(){
  console.log(user.username);
  let response = await fetch("http://localhost:8080/members/login",{
    method:"POST",
    headers:{
      "Accept":"application/json",
      "Content-type":"application/x-www-form-urlencoded"
    },
    body:`username=${user.username}&password=${user.password}`
  });
  let json = await response.json();
  console.log(json);

  // ** 로그인하고 나서 전역 객체(global object)에 로그인한 인증 정보를 담기 **
  // 자바의 entity 값과 같게 반환해줘야 한다.
  userDetails.username = json.result.userName;
  userDetails.password = json.result.pwd;
  userDetails.email = json.result.email;

  // router.push로서 사용하는 방법 
  router.push("/index");
}
</script>

<template>
    <main>
      <div class="sign-in">
        <div class="sign-in-logo">
          <img src="/image/logo-black.svg" alt="Rland" />
        </div>
        <form class="sign-in-form">
          <div class="sign-in-form-input">
            <div>
              <input
                type="text"
                class="input-bottom-line"
                placeholder="아이디"
                required
                v-model="user.username"
              />
            </div>
            <div>
              <input
                type="password"
                class="input-bottom-line"
                placeholder="비밀번호"
                required
                v-model="user.password"
              />
            </div>
          </div>

          <div class="sign-in-form-button">
            <div class="wd-100">
              <input type="submit" value="로그인" class="btn btn-default" @click.prevent="logHandler"/>
            </div>
            <div class="font-14">또는</div>
            <div class="wd-100">
              <a href="" class="deco icon-logo-google btn btn-outline"
                >구글로 로그인</a
              >
            </div>
          </div>
        </form>
        <div class="sign-in-find-user font-14">
          <a href="/sign-up.html">회원가입</a> |
          <a href="">비밀번호 찾기</a>
        </div>
      </div>
    </main>

</template>

<style scoped>
    @import url("/css/sign-in.css")
</style>


  • Header.vue

<script setup>
  // import userDetails from '../../stores/UserDetails';
  import {useUserDetailsStore} from '../../stores/useUserDetailsStore';
  import {useRouter} from 'vue-router';

  let router = new useRouter();
  
  let userDetails = useUserDetailsStore();

  async function logoutHandler(){
    console.log("logout");
    
    // ** logout 처리 ***
    // userDetails.id = 0;
    // userDetails.username = null;
    // userDetails.email = null;
    userDetails.logout();

    router.push('/index');
  }
</script>
<template>
    <header class="header-container">
      <h1 class="d-none">알랜드</h1>
      <div>
        <router-link to="/index" title="index페이지로 이동합니다.">
          <img src="/image/logo.svg" />
        </router-link>
        <router-link to="/admin/menu/list" class="icon icon-setting" title="메뉴관리로 이동합니다.">
          메뉴관리
        </router-link>
        <a class="icon icon-menu">메뉴</a>
      </div>
      <nav class="nav-wrap">
        <h1 class="d-none">네비게이션 목록</h1>
        <ul>
          <li>
            <a
              href="/index"
              class="icon icon-home"
              title="index페이지로 이동합니다."
              ></a
            >
          </li>
          <li>  <!-- v-show는 DOM이 있지만 보이지만 않는다. v-if는 DOM이 아예없어서 보이지 않는다. -->
                <!-- pinia store 대로 '' null로 바꾸자  -->
            <router-link v-if="!userDetails.username" to="/login" class="icon icon-sign-on" title="로그인">로그인</router-link>
            <router-link v-if="userDetails.username" to="/logout" class="icon icon-sign-out" title="로그아웃" @click="logoutHandler">로그아웃</router-link>
          </li>
        </ul>
      </nav>
    </header>
</template>




3) 로그아웃 기능에서 Pinia 사용 방법

1) getters 이용 방법 : Pinia 기능

  • Pinia의 getters 이용 방법 : 계산하는 식도 사용할 수 있거나 ‘속성’으로도 사용할 수 있다.

  • 속성이라서 함수의 인자를 넘겨 받을 수 없다.


  • useUserDetailsStore.js
import {defineStore} from "pinia";

// export const로 하면, 옵션s API로 기능이 사용된다. 즉, 함수인 ()로 사용해야 한다.
// export default로 쓰면, ()를 쓰면 안 된다! 변수도 지우자
// 하지만 export const가 기본이다!
export const useUserDetailsStore = defineStore("userDetails", {
    state:()=>({
        id:0,
        username:null,
        email:null,
        role:null
    }),

    // Pinia의 getters 이용 방법 : 계산하는 식도 사용할 수 있거나 '속성'으로도 사용할 수 있다.
    // 속성이라서 함수의 인자를 넘겨 받을 수 없다. 
    getters:{
        isAuthenticated:(state)=>state.username==null?false:true
    },

});


2) actions 이용 방법 : Pinia 기능

  • actions : 함수의 인자를 넘겨 받을 수 있다. 로그아웃 기능에서 사용 가능


  • useUserDetailsStore.js
import {defineStore} from "pinia";

// export const로 하면, 옵션s API로 기능이 사용된다. 즉, 함수인 ()로 사용해야 한다.
// export default로 쓰면, ()를 쓰면 안 된다! 변수도 지우자
// 하지만 export const가 기본이다!
export const useUserDetailsStore = defineStore("userDetails", {
    state:()=>({
        id:0,
        username:null,
        email:null,
        role:null
    }),

    // Pinia의 getters 이용 방법 : 계산하는 식도 사용할 수 있거나 '속성'으로도 사용할 수 있다.
    // 속성이라서 함수의 인자를 넘겨 받을 수 없다. 
    getters:{
        isAuthenticated:(state)=>state.username==null?false:true
    },

    // actions : 함수의 인자를 넘겨 받을 수 있다. 로그아웃 기능에서 사용 가능
    actions:{
        logout(){
            this.id=0;
            this.username =null;
            this.email =null;
        }
    }

});


c. 로그아웃 실습코드 : pinia 이용


  • useUserDetailsStore.js
import {defineStore} from "pinia";

// export const로 하면, 옵션s API로 기능이 사용된다. 즉, 함수인 ()로 사용해야 한다.
// export default로 쓰면, ()를 쓰면 안 된다! 변수도 지우자
// 하지만 export const가 기본이다!
export const useUserDetailsStore = defineStore("userDetails", {
    state:()=>({
        id:0,
        username:null,
        email:null,
        role:null
    }),

    // Pinia의 getters 이용 방법 : 계산하는 식도 사용할 수 있거나 '속성'으로도 사용할 수 있다.
    // 속성이라서 함수의 인자를 넘겨 받을 수 없다. 
    getters:{
        isAuthenticated:(state)=>state.username==null?false:true
    },

    // actions : 함수의 인자를 넘겨 받을 수 있다. 로그아웃 기능에서 사용 가능
    actions:{
        logout(){
            this.id=0;
            this.username =null;
            this.email =null;
        }
    }

    // *** 권한 : 허락받지 않는 페이지로 이동해서는 안 된다. 
    // 그래서, 실습을 위해 우리는 허락 받지 않은 페이지가 필요하다!
});


  • Header.vue

<script setup>
  // import userDetails from '../../stores/UserDetails';
  import {useUserDetailsStore} from '../../stores/useUserDetailsStore';
  import {useRouter} from 'vue-router';

  let router = new useRouter();
  
  let userDetails = useUserDetailsStore();

  async function logoutHandler(){
    console.log("logout");
    
    // ** logout 처리 ***
    // userDetails.id = 0;
    // userDetails.username = null;
    // userDetails.email = null;
    userDetails.logout();

    router.push('/index');
  }
</script>
<template>
    <header class="header-container">
      <h1 class="d-none">알랜드</h1>
      <div>
        <router-link to="/index" title="index페이지로 이동합니다.">
          <img src="/image/logo.svg" />
        </router-link>
        <router-link to="/admin/menu/list" class="icon icon-setting" title="메뉴관리로 이동합니다.">
          메뉴관리
        </router-link>
        <a class="icon icon-menu">메뉴</a>
      </div>
      <nav class="nav-wrap">
        <h1 class="d-none">네비게이션 목록</h1>
        <ul>
          <li>
            <a
              href="/index"
              class="icon icon-home"
              title="index페이지로 이동합니다."
              ></a
            >
          </li>
          <li>  <!-- v-show는 DOM이 있지만 보이지만 않는다. v-if는 DOM이 아예없어서 보이지 않는다. -->
                <!-- pinia store 대로 '' null로 바꾸자  -->
            <router-link v-if="!userDetails.username" to="/login" class="icon icon-sign-on" title="로그인">로그인</router-link>
            <router-link v-if="userDetails.username" to="/logout" class="icon icon-sign-out" title="로그아웃" @click="logoutHandler">로그아웃</router-link>
          </li>
        </ul>
      </nav>
    </header>
</template>




4) Vue.js 권한 기능

  • 권한** : 허락받지 않는 페이지로 이동해서는 안 된다.

  • 그래서, 실습을 위해 우리는 허락 받지 않은 페이지가 필요하다!

  • Admin이 이용하는 MenuList 페이지를 구성한다.


a. 실습코드

  • Layout.vue
<script setup>
import Header from '../../inc/Header.vue';
import Aside from './Aside.vue';
import Footer from '../../inc/Footer.vue';


</script>

<template>
    <!-- <Header/> 자동 임포트 가능하다. -->
    <!-- Header-vue라고 쓰는 경우도 있는데 컴포넌트 이름 그대로 가는 것이 식별하기 쉽다. -->
    <Header/>
    <Aside/>
    <router-view></router-view>
    <Footer/>

    <!-- 메인페이지 가운데 빗금 문양 -->
    <div class="slash-background">
        <div></div>
        <div></div>
        <div></div>
      </div>
</template>

<style scoped>
    main{
        background-color: var(--color-bg-2);
    }
</style>


  • List.vue
<template>
    <!-- ----------------------메인---------------------------- -->
    <main>
      <div class="tab-search-container">
        <div class="top-img-title">
          <h1>메뉴 관리</h1>
        </div>
        <!-- ----------------메뉴  -->
        <div class="tab-search-wrap">
          <section class="tab-section">
            <h1> 목록</h1>
            <ul>
              <li class="selected"><a href="">전체메뉴</a></li>
              <li><a href="">커피</a></li>
              <li><a href="">수제청</a></li>
              <li><a href="">샌드위치</a></li>
              <li><a href="">쿠키</a></li>
            </ul>
          </section>
          <!-- --------------------------------------------------검색바 -->
          <section class="search-section">
            <h1>검색</h1>
            <form action="">
              <input type="text" value="" name="search" />
              <button type="submit" class="icon icon-search"></button>
            </form>
          </section>
        </div>
      </div>
      <div class="flex-align">
        <!-- ----------------상품추가 박스 모바일 -->
        <a href="../menu/reg.html" class="menu-add-box-mobile">
          <span class="icon icon-plus-circle"></span>
        </a>
        <!-- ----------------메뉴 목록 -->
        <section class="menu-edit-section">
          <h1 class="d-none">메뉴 목록</h1>
          <!-- ----------------상품추가 박스 pc -->
          <a href="../menu/reg.html" class="menu-add-box-pc">
            <span class="icon icon-plus-circle"></span>
          </a>

          <section class="menu">
            <div>
              <a href="../../anonymous/menu/detail.html">
                <img src="/image/espresso.svg" alt="에스프레소" />
              </a>
            </div>
            <h1>에스프레소</h1>
            <h2>Espresso</h2>
            <div>2,500</div>
            <div>
              <a class="icon icon-arrow-up">추천메뉴 보기</a>
            </div>
            <div>
              <a href="" class="icon icon-lg icon-pencil">수정</a>
              <a href="" class="icon icon-lg icon-trash">삭제</a>
            </div>
          </section>
          <!-- ----------------추천메뉴 목록 -->
          <section class="recommend-menu-section">
            <div class="bl-2 pl-12 flex-cloumn-gap-12">
              <h1>추천 메뉴</h1>
              <section class="menu-delete-section">
                <h1 class="d-none">메뉴 목록</h1>
                <section class="menu">
                  <div>
                    <a href="">
                      <img src="/image/espresso.svg" alt="에스프레소" />
                    </a>
                  </div>
                  <h1>에스프레소</h1>
                  <h2>Espresso</h2>
                  <div>2,500</div>
                  <div>
                    <a href="" class="icon icon-trash">삭제</a>
                  </div>
                </section>
                <section class="menu">
                  <div>
                    <a href="">
                      <img src="/image/espresso.svg" alt="에스프레소" />
                    </a>
                  </div>
                  <h1>에스프레소</h1>
                  <h2>Espresso</h2>
                  <div>2,500</div>
                  <div>
                    <a href="" class="icon icon-trash">삭제</a>
                  </div>
                </section>
                <section class="menu">
                  <div>
                    <a href="">
                      <img src="/image/espresso.svg" alt="에스프레소" />
                    </a>
                  </div>
                  <h1>에스프레소</h1>
                  <h2>Espresso</h2>
                  <div>2,500</div>
                  <div>
                    <a href="" class="icon icon-trash">삭제</a>
                  </div>
                </section>

                <!-- ----------------상품추가 박스 pc small-->
                <a href="../menu/reg.html" class="menu-add-box-pc-sm">
                  <span class="icon icon-plus-circle"></span>
                </a>
                <!-- ----------------상품추가 박스 모바일 -->
                <a href="../menu/reg.html" class="menu-add-box-mobile">
                  <span class="icon icon-plus-circle"></span>
                </a>
              </section>
            </div>
          </section>

          <section class="menu">
            <div>
              <a href="../../anonymous/menu/detail.html">
                <img src="/image/espresso.svg" alt="에스프레소" />
              </a>
            </div>
            <h1>에스프레소</h1>
            <h2>Espresso</h2>
            <div>2,500</div>
            <div>
              <a class="icon icon-arrow-up">추천메뉴 보기</a>
            </div>
            <div>
              <a href="" class="icon icon-lg icon-pencil">수정</a>
              <a href="" class="icon icon-lg icon-trash">삭제</a>
            </div>
          </section>

          <section class="menu">
            <div>
              <a href="../../anonymous/menu/detail.html">
                <img src="/image/espresso.svg" alt="에스프레소" />
              </a>
            </div>
            <h1>에스프레소</h1>
            <h2>Espresso</h2>
            <div>2,500</div>
            <div>
              <a class="icon icon-arrow-up">추천메뉴 보기</a>
            </div>
            <div>
              <a href="" class="icon icon-lg icon-pencil">수정</a>
              <a href="" class="icon icon-lg icon-trash">삭제</a>
            </div>
          </section>

          <section class="menu">
            <div>
              <a href="">
                <img src="/image/espresso.svg" alt="에스프레소" />
              </a>
            </div>
            <h1>에스프레소</h1>
            <h2>Espresso</h2>
            <div>2,500</div>
            <div>
              <a class="icon icon-arrow-up">추천메뉴 보기</a>
            </div>
            <div>
              <a href="" class="icon icon-lg icon-pencil">수정</a>
              <a href="" class="icon icon-lg icon-trash">삭제</a>
            </div>
          </section>

          <section class="menu">
            <div>
              <a href="">
                <img src="/image/espresso.svg" alt="에스프레소" />
              </a>
            </div>
            <h1>에스프레소</h1>
            <h2>Espresso</h2>
            <div>2,500</div>
            <div>
              <a class="icon icon-arrow-up">추천메뉴 보기</a>
            </div>
            <div>
              <a href="" class="icon icon-lg icon-pencil">수정</a>
              <a href="" class="icon icon-lg icon-trash">삭제</a>
            </div>
          </section>
        </section>
      </div>
    </main>
</template>

<style scoped>
    @import url(/css/menu.css);
    @import url(/css/admin/menu/list.css);
</style>

2. 권한 : 230509

1) 권한 부여하는 과정 : Pinia

a.

  • useRoute는 전송 정보이다!

  • composition API로 코드가 변경되면서 JS 코드가 읽히면서 바로 로드가 된다.

  • composition API에서는 created 단계가 없어지면서 바로 모든 코드가 로드된다.




b. 실습 코드 :

  • useUserDetailsStore.js
import {defineStore} from "pinia";

// export const로 하면, 옵션s API로 기능이 사용된다. 즉, 함수인 ()로 사용해야 한다.
// export default로 쓰면, ()를 쓰면 안 된다! 변수도 지우자
// 하지만 export const가 기본이다!
export const useUserDetailsStore = defineStore("userDetails", {
    state:()=>({
        id:0,
        username:null,
        email:null,
        // 권한도 역할이 여러개 필요한 경우에 이렇게 배열로 받는다!
        roles:[] // ["ADMIN","TEACHER"]
    }), 

    // Pinia의 getters 이용 방법 : 계산하는 식도 사용할 수 있거나 '속성'으로도 사용할 수 있다.
    // 속성이라서 함수의 인자를 넘겨 받을 수 없다. 
    getters:{
        isAuthenticated:(state)=>state.username==null?false:true
    },

    // actions : 함수의 인자를 넘겨 받을 수 있다. 로그아웃 기능에서 사용 가능
    actions:{
        logout(){
            this.id=0;
            this.username =null;
            this.email =null;
        },
        hasRole(role){
            // 여기서 role은 ["ADMIN","TEACHER"]가 되어 role을 찾는 작업이 필요!
            // 해당 역할이 없으면 false 있으면 true 반환!
            let result = this.roles.indexOf(role)<0?false:true;
            return result;
        }

    }

    // *** 권한 : 허락받지 않는 페이지로 이동해서는 안 된다. 
    // 그래서, 실습을 위해 우리는 허락 받지 않은 페이지가 필요하다!
});


  • MemberController.java
    • 권한 중요 : dto.put("roles", new String[]{"ADMIN","TEACHER"});

@RestController
@RequestMapping("members") 
public class MemberController {

    @Autowired
    private MemberService service;

    @PostMapping("login")
    public ResponseEntity<Map<String, Object>> login(
        String username, 
        String password){

        System.out.println(username);
        Map<String, Object> dto = new HashMap<>();

        dto.put("result",null);

        // ** DB 이용시 사용하기! **
        if(service.isValid(username, password)){
            Member member = service.getUsername(username);
            dto.put("result", member);
            dto.put("roles", new String[]{"ADMIN","TEACHER"});	
            // 권한 중요!
        }

        return new ResponseEntity<Map<String, Object>>(dto, HttpStatus.OK);
    }
}



  • Login.vue

<script setup>
import {reactive} from 'vue';
import { useRouter, useRoute } from 'vue-router';
// useRoute는 전송 정보이다!

// 전역 변수를 선언! store에서 가져온다.
// 하나의 모듈에다가 만들어서 공유하면 전역변수가 된다. 
// 모두다 같은 객체를 공유하는지가 궁금하다! 그래서, 여기서 담아서 다른페이지에서 읽어보자!!
// import userDetails from '../stores/UserDetails.js';

// ** pinia : 상태관리 전역 변수! **
import {useUserDetailsStore} from '../stores/useUserDetailsStore';
// import된 pinia 상태 관리 변수를 함수로 만들어서 사용한다. 
// 여기서 ()가 'state:()=>{}' 를 의미한다. 
// 인증된 정보를 저장하기 위해서 SSR에서는 Session이나 쿠키를 사용했는데
// CSR에서는 session이나 쿠키가 필요없다. 페이지를 갱신하지 않으면 전역변수가 존재해서 편리하다. 
let userDetails = useUserDetailsStore();

let router = useRouter();

// useRoute는 전송 정보이다!
// composition API로 코드가 변경되면서 JS 코드가 읽히면서 바로 로드가 된다. 
// composition API에서는 created 단계가 없어지면서 바로 모든 코드가 로드된다.
let route = useRoute(); 

let user = reactive({
  username:"",
  password:"",
  roles:""
});

async function logHandler(){
  console.log(user.username);
  let response = await fetch("http://localhost:8080/members/login",{
    method:"POST",
    headers:{
      "Accept":"application/json",
      "Content-type":"application/x-www-form-urlencoded"
    },
    body:`username=${user.username}&password=${user.password}`
  });
  let json = await response.json();
  console.log(json);

  // ** 로그인하고 나서 전역 객체(global object)에 로그인한 인증 정보를 담기 **
  // 자바의 entity 값과 같게 반환해줘야 한다.
  userDetails.username = json.result.userName;
  // userDetails.password = json.rescult.pwd;
  userDetails.email = json.result.email;
  userDetails.roles = json.roles;

  // returnURL 사용방법 : 이동할 때, Route 정보 얻기!

  let returnURL = route.query.returnURL;

  if(returnURL)
    router.push(returnURL);
  else
    router.push("/index");
  
  
    // router.push로서 사용하는 방법 
  // router.push("/index");
}
</script>

<template>
    <main>
      <div class="sign-in">
        <div class="sign-in-logo">
          <img src="/image/logo-black.svg" alt="Rland" />
        </div>
        <form class="sign-in-form">
          <div class="sign-in-form-input">
            <div>
              <input
                type="text"
                class="input-bottom-line"
                placeholder="아이디"
                required
                v-model="user.username"
              />
            </div>
            <div>
              <input
                type="password"
                class="input-bottom-line"
                placeholder="비밀번호"
                required
                v-model="user.password"
              />
            </div>
          </div>

          <div class="sign-in-form-button">
            <div class="wd-100">
              <input type="submit" value="로그인" class="btn btn-default" @click.prevent="logHandler"/>
            </div>
            <div class="font-14">또는</div>
            <div class="wd-100">
              <a href="" class="deco icon-logo-google btn btn-outline"
                >구글로 로그인</a
              >
            </div>
          </div>
        </form>
        <div class="sign-in-find-user font-14">
          <a href="/sign-up.html">회원가입</a> |
          <a href="">비밀번호 찾기</a>
        </div>
      </div>
    </main>

</template>

<style scoped>
    @import url("/css/sign-in.css")
</style>


  • route.js
import Index from './Index.vue'
import Layout from './inc/Layout.vue'
import MenuList from './menu/List.vue'

import {useUserDetailsStore} from '../../stores/useUserDetailsStore.js';

const admin = { 
    path: '/admin',
    component: Layout,
    children:[
        {
            path:'index', 
            component:Index
        },
        {
        path:'menu', children:
            [
                {
                    path: 'list', component: MenuList
                }
            ]
        }
    ],
    beforeEnter(to, from, next){
        console.log("어딜가시려나요?");
        console.log(`to:${to.path}, from:${from.path}`);

        let userDetails = useUserDetailsStore();
        
        console.log(userDetails.isAuthenticated);

        let url = `/login?returnURL=${to.path}`;

        if(!userDetails.isAuthenticated)
            next(url);
        // 권한에 대한 조건이 1개 더 필요! 에러 페이지도 추가로 필요!!**
        else if(!userDetails.hasRole("ADMIN"))   
            next("/error/403");
        else
            next();
    }
}
  
export default admin;



2) 에러를 대신 해주는 페이지 구현

a. 페이지 구성

  • 403.vue
<template>
    <main>
        <h2>에러 403: 권한이 없는 페이지를 접근하였습니다.</h2>
    </main>
</template>


  • 404.vue

<template>
    <main>
        <h2>에러 404 : 요청하신 페이지가 존재하지 않습니다.</h2>
    </main>
</template>



b. 경로 구성

  • route.js
    • Lazy Loading Routes도 이용할 수 있다.
    • 느슨한 화면 생성
    • 화면이 조건에 의해 필요할 때만 import 시킨다.
import Layout from './inc/Layout.vue'
import Index from './Index.vue'
import Login from './Login.vue'

// import Err403 from './error/403.vue'
import NotFound from './error/404.vue'

const root = { path: '/', component: Layout, children:[
    { path: 'index', component: Index},
    { path: 'login', component: Login},
    {
        path:'error', children:[
            // 'Lazy Loading Routes'
            // 모든 페이지를 처음에 다 로드하지 않을 때 사용 : 느슨한 화면 생성
            // 화면이 조건에 의해 필요할 때만 import 시킨다. 
            // 하지만, 모든 것을 다 Lasy로 구현해서는 안된다.
            {path:"403", component:()=> import('./error/403.vue')}
        ]
    },
    {   
        // 무엇이든 일치시키려면 매개변수 바로 뒤에 괄호 안에 정규 표현식을 추가하여 사용자 정의 매개변수 정규 표현식을 사용할 수 있습니다 .
        // 일치하는 것이 없으면, 404 에러로 반환!
        path:"/:pathMatch(.*)*",
        component:NotFound
    }
]}

export default root;



3) Vue의 css Transition

  • admin/menu/List.vue
<script setup>
import {ref} from 'vue'
  let showRcmdMenu = ref(false);
</script>

<template>
    <!-- ----------------------메인---------------------------- -->
    <main>
      <div class="tab-search-container">
        <div class="top-img-title">
          <h1>메뉴 관리</h1>
        </div>
        <!-- ----------------메뉴  -->
        <div class="tab-search-wrap">
          <section class="tab-section">
            <h1> 목록</h1>
            <ul>
              <li class="selected"><a href="">전체메뉴</a></li>
              <li><a href="">커피</a></li>
              <li><a href="">수제청</a></li>
              <li><a href="">샌드위치</a></li>
              <li><a href="">쿠키</a></li>
            </ul>
          </section>
          <!-- --------------------------------------------------검색바 -->
          <section class="search-section">
            <h1>검색</h1>
            <form action="">
              <input type="text" value="" name="search" />
              <button type="submit" class="icon icon-search"></button>
            </form>
          </section>
        </div>
      </div>
      <div class="flex-align">
        <!-- ----------------상품추가 박스 모바일 -->
        <a href="../menu/reg.html" class="menu-add-box-mobile">
          <span class="icon icon-plus-circle"></span>
        </a>
        <!-- ----------------메뉴 목록 -->
        <section class="menu-edit-section">
          <h1 class="d-none">메뉴 목록</h1>
          <!-- ----------------상품추가 박스 pc -->
          <a href="../menu/reg.html" class="menu-add-box-pc">
            <span class="icon icon-plus-circle"></span>
          </a>

          <section class="menu">
            <div>
              <a href="../../anonymous/menu/detail.html">
                <img src="/image/espresso.svg" alt="에스프레소" />
              </a>
            </div>
            <h1>에스프레소</h1>
            <h2>Espresso</h2>
            <div>2,500</div>
            <div>
              <a class="icon icon-arrow-up" @click.prevent="showRcmdMenu = !showRcmdMenu">추천메뉴 보기</a>
            </div>
            <div>
              <a href="" class="icon icon-lg icon-pencil">수정</a>
              <a href="" class="icon icon-lg icon-trash">삭제</a>
            </div>
          </section>
          <!-- ----------------추천메뉴 목록 -->
          <Transition>
          <section class="recommend-menu-section" v-if="showRcmdMenu">
            <div class="bl-2 pl-12 flex-cloumn-gap-12">
              <h1>추천 메뉴</h1>
              <section class="menu-delete-section">
                <h1 class="d-none">메뉴 목록</h1>
                <section class="menu">
                  <div>
                    <a href="">
                      <img src="/image/espresso.svg" alt="에스프레소" />
                    </a>
                  </div>
                  <h1>에스프레소</h1>
                  <h2>Espresso</h2>
                  <div>2,500</div>
                  <div>
                    <a href="" class="icon icon-trash">삭제</a>
                  </div>
                </section>
                <section class="menu">
                  <div>
                    <a href="">
                      <img src="/image/espresso.svg" alt="에스프레소" />
                    </a>
                  </div>
                  <h1>에스프레소</h1>
                  <h2>Espresso</h2>
                  <div>2,500</div>
                  <div>
                    <a href="" class="icon icon-trash">삭제</a>
                  </div>
                </section>
                <section class="menu">
                  <div>
                    <a href="">
                      <img src="/image/espresso.svg" alt="에스프레소" />
                    </a>
                  </div>
                  <h1>에스프레소</h1>
                  <h2>Espresso</h2>
                  <div>2,500</div>
                  <div>
                    <a href="" class="icon icon-trash">삭제</a>
                  </div>
                </section>

                <!-- ----------------상품추가 박스 pc small-->
                <a href="../menu/reg.html" class="menu-add-box-pc-sm">
                  <span class="icon icon-plus-circle"></span>
                </a>
                <!-- ----------------상품추가 박스 모바일 -->
                <a href="../menu/reg.html" class="menu-add-box-mobile">
                  <span class="icon icon-plus-circle"></span>
                </a>
              </section>
            </div>
          </section>
        </Transition>
          <section class="menu">
            <div>
              <a href="../../anonymous/menu/detail.html">
                <img src="/image/espresso.svg" alt="에스프레소" />
              </a>
            </div>
            <h1>에스프레소</h1>
            <h2>Espresso</h2>
            <div>2,500</div>
            <div>
              <a class="icon icon-arrow-up">추천메뉴 보기</a>
            </div>
            <div>
              <a href="" class="icon icon-lg icon-pencil">수정</a>
              <a href="" class="icon icon-lg icon-trash">삭제</a>
            </div>
          </section>

          <section class="menu">
            <div>
              <a href="../../anonymous/menu/detail.html">
                <img src="/image/espresso.svg" alt="에스프레소" />
              </a>
            </div>
            <h1>에스프레소</h1>
            <h2>Espresso</h2>
            <div>2,500</div>
            <div>
              <a class="icon icon-arrow-up">추천메뉴 보기</a>
            </div>
            <div>
              <a href="" class="icon icon-lg icon-pencil">수정</a>
              <a href="" class="icon icon-lg icon-trash">삭제</a>
            </div>
          </section>

          <section class="menu">
            <div>
              <a href="">
                <img src="/image/espresso.svg" alt="에스프레소" />
              </a>
            </div>
            <h1>에스프레소</h1>
            <h2>Espresso</h2>
            <div>2,500</div>
            <div>
              <a class="icon icon-arrow-up">추천메뉴 보기</a>
            </div>
            <div>
              <a href="" class="icon icon-lg icon-pencil">수정</a>
              <a href="" class="icon icon-lg icon-trash">삭제</a>
            </div>
          </section>

          <section class="menu">
            <div>
              <a href="">
                <img src="/image/espresso.svg" alt="에스프레소" />
              </a>
            </div>
            <h1>에스프레소</h1>
            <h2>Espresso</h2>
            <div>2,500</div>
            <div>
              <a class="icon icon-arrow-up">추천메뉴 보기</a>
            </div>
            <div>
              <a href="" class="icon icon-lg icon-pencil">수정</a>
              <a href="" class="icon icon-lg icon-trash">삭제</a>
            </div>
          </section>
        </section>
      </div>
    </main>
</template>

<style scoped>
  @import url(/css/menu.css);
  @import url(/css/admin/menu/list.css);

  /* vue용 css 트랜지션 이용!*** */
  /* 여기 v-로 시작하는 태그는 익명의 이름으로 스타일을 줄 때, 사용한다. */
  /* 이름을 부여하고 싶으면 Transition 속성인 name 속성에다가 추가한다.  */
  .v-enter-active,
  .v-leave-active {
    transition: opacity 0.5s ease;
  }

  .v-enter-from,
  .v-leave-to {
    opacity: 0;
  }
</style>



4. Vue3 css Transition : 230510

  • 페이지 이동시 마다 Transition 이벤트 걸어주기!
<script setup>
import Header from './Header.vue';
import Footer from './Footer.vue';
import Aside from './Aside.vue';
</script>

<template>
    <Header />
    <Aside />
        <Transition>
           <router-view></router-view>
        </Transition>
    <Footer />
      <!-- 메인페이지 가운데 빗금 문양 -->
      <div class="slash-background">
        <div></div>
        <div></div>
        <div></div>
      </div>
</template>

<style scoped>

.v-enter-active,
.v-leave-active {
  transition: opacity 3s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}



</style>



5. OAuth2 : 230512

1) Open Auth 개념

  • Vue3에서 이전에는 구글 로그인을 위해서 vue-google-oauth2 라이브러리를 이용했지만 더 이상 지원하지 않아서

  • 이제는 vue3-google-login 라이브러리를 이용하기!!!**

  • 추후에는 Spring Boot에서 구글 로그인도 할 예정이다.


a. 변경 되기 전의 라이브러리

  • 이미지


b. 변경된 후의 라이브러리

  • 이미지



2) Google OAuth2 설정하기

  • console.cloud.google.com 사이트에 들어가서 설정하기



a. ‘콘솔 클라우드 구글’에서 ‘Google OAuth2’ 설정하기 1

  • ‘콘솔 클라우드 구글’에서 ‘새 프로젝트 설정’하기


  • 이미지


  • 이미지


  • 이미지


  • 이미지


  • 이미지


b. ‘콘솔 클라우드 구글’에서 ‘Google OAuth2’ 설정하기 2

  • ‘콘솔 클라우드 구글’에서 ‘Oauth 동의 화면’ 설정하기


  • 이미지


  • 이미지


  • 이미지


c. ‘콘솔 클라우드 구글’에서 ‘Google OAuth2’ 설정하기 3

  • ‘콘솔 클라우드 구글’에서 ‘범위 추가/삭제’ 설정하기


  • 이미지


  • 이미지


  • 이미지


  • 이미지


d. ‘콘솔 클라우드 구글’에서 ‘Google OAuth2’ 설정하기 4

  • ‘콘솔 클라우드 구글’에서 ‘동의 화면 설정’ 마무리 하기


  • 이미지


  • 이미지


  • 이미지


  • 이미지



3) 승인된 자바스크립트 원본

  • origins이 자바스크립트 출처를 의미한다.

  • 실제 브라우저의 port 번호도 같이 입력해줘야한다.


a. ‘콘솔 클라우드 구글’에서 추가로 Authorized Javascript origins 설정하기

  • Javascript 이용 시, 승인된 경로를 설정해주는 것


  • 이미지


  • 이미지


  • 이미지


  • 이미지



b. ‘콘솔 클라우드 구글’에서 Authorized Javascript origins 설정 마무리하기

  • Javascript 이용 시, 승인된 경로를 설정해주는 것 마무리!


  • 이미지


  • 이미지


  • 이미지



4) Google OAuth2 설정이 끝나면, 다시 Vue 설정

a. Vue3에서 구글 로그인 설정 방법

  • 이미지


  • 이미지


  • 이미지


  • 이미지


  • 이미지


  • 이미지



5) Vue 설정이 끝나면 다시 구글 로그인 로직 구현하기

a. Vue3에서 구글 로그인 확인하기

  • 이미지


  • 이미지


  • 이미지


  • 이미지


b. Vue3에서 google 아이디의 credential 정보 조회

  • 이미지


  • 이미지


  • 이미지


  • 이미지


  • 이미지


  • 이미지


c. Vue3에서 구글 로그인의 로그아웃 과정 설정

  • 이미지


  • 이미지


d. Vue3에서 구글 로그인 코드 모음

a) 구글 로그인 라이브러리 설정 추가
  • main.js

import { createApp } from "vue";
import { createRouter, createWebHashHistory } from "vue-router";
import { createPinia } from "pinia";
import App from "./App.vue";
import vue3GoogleLogin from "vue3-google-login";

import rootRoute from "./components/route.js";
import adminRoute from "./components/admin/route.js";

const routes = [rootRoute, adminRoute];

const router = createRouter({
  history: createWebHashHistory(),
  routes // short for `routes: routes`
});

const pinia = createPinia();

//Vue.createApp() -> Global Library
createApp(App)
  .use(router)
  .use(vue3GoogleLogin, {
    clientId:
      "738123608486-vrhl0f8mg5fu6oqm8ubpt4uhav83g29k.apps.googleusercontent.com"
  })
  .use(pinia)
  .mount("#app"); //-> ES6 Library




b) 로그인을 위한 구글 로그인 페이지 설정

  • ‘기존 로그인 로직’과 별개로 ‘구글 로그인 로직’이 동작하게 한다.


  • Login.vue
<script setup>
import { reactive } from 'vue';
import { useRouter, useRoute } from 'vue-router';
//전역변수가 필요한데 어떻게 하지? =>모듈만들어서 붙인다 
//import userDetails from '../stores/UserDeatails.js';
import { useUserDetailsStore } from '../stores/useUserDetailsStore';
import { decodeCredential } from 'vue3-google-login'


let userDetails = useUserDetailsStore();
let router = useRouter();
let route = useRoute();

let user = reactive({
  username:"",
  password:"",
  roles:"",

})

function googleLoginHandler(response){
  let userData = decodeCredential(response.credential);

  console.log(userData);
  userDetails.username = userData.name;
  userDetails.email = userData.email;
  userDetails.roles=["ADMIN","MEMBER"];


  let returnURL = route.query.returnURL;
  
  if(returnURL)
    router.push(returnURL);
  else
    router.push("/index");

  
}

async function loginHandler(){
  console.log(user.username)
  let response = await fetch("http://localhost:8080/members/login",{
    method:"POST",
    headers:{
      "Accept": "application/json",
      "Content-type":"application/x-www-form-urlencoded"
    },
    body:`username=${user.username}&password=${user.password}`
  });
  let json = await response.json();
  console.log(json)
  //console.log(json.result.email);
  //전역 객체(global object)에 인증 정보를 담기
  userDetails.username = json.result.userName;
  userDetails.email = json.result.email;
  userDetails.roles=json.roles;


  let returnURL = route.query.returnURL;
  
  if(returnURL)
    router.push(returnURL);
  else
    router.push("/index");
}

</script>


<template>
    <main>
      <div class="sign-in">
        <div class="sign-in-logo">
          <img src="/image/logo-black.svg" alt="Rland" />
        </div>
        <form class="sign-in-form">
          <div class="sign-in-form-input">
            <div>
              <input
                type="text"
                class="input-bottom-line"
                placeholder="아이디"
                required
                v-model="user.username"
              />
            </div>
            <div>
              <input
                type="password"
                class="input-bottom-line"
                placeholder="비밀번호"
                required
                v-model="user.password"
              />
            </div>
          </div>

          <div class="sign-in-form-button" >
            <div class="wd-100">
              <input type="submit" value="로그인" class="btn btn-default" @click.prevent="loginHandler" />
            </div>
            <div class="font-14">또는</div>
            <div class="wd-100">
              <!-- <a href="" class="deco icon-logo-google btn btn-outline"
                >구글로 로그인</a
              > -->
              <GoogleLogin :callback="googleLoginHandler" />
            </div>
          </div>
        </form>
        <div class="sign-in-find-user font-14">
          <a href="./sign-up.html">회원가입</a> |
          <a href="">비밀번호 찾기</a>
        </div>
      </div>
    </main>

</template>
<style scoped>
    @import url("/css/sign-in.css");
    main{

    }
</style>



c) 구글 로그인에 대한 구글 로그아웃 구현
  • Header.vue
<script setup>
  // import userDetails from '../../stores/UserDetails';
  import {useUserDetailsStore} from '../../stores/useUserDetailsStore';
  import {useRouter} from 'vue-router';
  import { googleLogout } from 'vue3-google-login';

  let router = new useRouter();
  
  let userDetails = useUserDetailsStore();

  async function logoutHandler(){
    console.log("logout");
    
    // ** logout 처리 ***
    // userDetails.id = 0;
    // userDetails.username = null;
    // userDetails.email = null;
    userDetails.logout();

    // 로그아웃할 때, 구글 로그인 정보를 지워야하는 더 필요하다!
    googleLogout();

    router.push('/index');
  }
</script>
<template>
    <header class="header-container">
      <h1 class="d-none">알랜드</h1>
      <div>
        <router-link to="/index" title="index페이지로 이동합니다.">
          <img src="/image/logo.svg" />
        </router-link>
        <a class="icon icon-menu">메뉴</a>
      </div>
      <nav class="nav-wrap">
        <h1 class="d-none">네비게이션 목록</h1>
        <ul>
          <li>
            <router-link
            to="/index"
            class="icon icon-home"
            title="index페이지로 이동합니다."
            >홈</router-link>
          </li>
          <li>
            <router-link to="/admin/menu/list" class="icon icon-setting" title="메뉴관리로 이동합니다.">
              메뉴관리
            </router-link>
          </li>
          <li>  <!-- v-show는 DOM이 있지만 보이지만 않는다. v-if는 DOM이 아예없어서 보이지 않는다. -->
                <!-- pinia store 대로 ''를 null로 바꾸자  -->
            <router-link v-if="!userDetails.username" to="/login" class="icon icon-sign-on" title="로그인">로그인</router-link>
            <router-link v-if="userDetails.username" to="/logout" class="icon icon-sign-out" title="로그아웃" @click="logoutHandler">로그아웃</router-link>
          </li>
        </ul>
      </nav>
    </header>
</template>


  • useUserDetailsStore.js
import {defineStore} from "pinia";

// export const로 하면, 옵션s API로 기능이 사용된다. 즉, 함수인 ()로 사용해야 한다.
// export default로 쓰면, ()를 쓰면 안 된다! 변수도 지우자
// 하지만 export const가 기본이다!
export const useUserDetailsStore = defineStore("userDetails", {
    state:()=>({
        id:0,
        username:null,
        email:null,
        // 권한도 역할이 여러개 필요한 경우에 이렇게 배열로 받는다!
        roles:[] // ["ADMIN","TEACHER"]
    }), 

    // Pinia의 getters 이용 방법 : 계산하는 식도 사용할 수 있거나 '속성'으로도 사용할 수 있다.
    // 속성이라서 함수의 인자를 넘겨 받을 수 없다. 
    getters:{
        isAuthenticated:(state)=>state.username==null?false:true
    },

    // actions : 함수의 인자를 넘겨 받을 수 있다. 로그아웃 기능에서 사용 가능
    actions:{
        logout(){
            this.id=0;
            this.username =null;
            this.email =null;
        },
        hasRole(role){
            // 여기서 role은 ["ADMIN","TEACHER"]가 되어 role을 찾는 작업이 필요!
            // 해당 역할이 없으면 false 있으면 true 반환!
            let result = this.roles.indexOf(role)<0?false:true;
            return result;
        }

    }

    // *** 권한 : 허락받지 않는 페이지로 이동해서는 안 된다. 
    // 그래서, 실습을 위해 우리는 허락 받지 않은 페이지가 필요하다!
});