21.04.01 lamplight서비스 프로젝트(ionic 기반으로 옮기기 위한 ionic exam template 진행(tailwind, fontawesome, 로그인,게시물리스팅 가능))

2021. 4. 1. 22:08Vue.js/Spring & Vue APP 프로젝트(프론트엔드)

# NOTE

PWA???
- "프로그레시브 웹 앱Progressive Web App(PWA)"
- PWA는 몇 가지 기능(예를 들어 ‘설치’ 기능)을 추가하여 전통적인 웹사이트를 좀 더 강화한 것
- PWA는 운영체제(따라서 그 사용자)와 깊은 수준에서 연결하는 능력을 갖고 있다. 
- 이는 설치, 그리고 알림이나 주소록 접근 등의 기능을 제공하는 API를 통해 가능하다.
ionic 테마사이트
https://ionicframework.com/docs/theming/themes
//camelcase??
    //일반적으로 객체 명명은 memberAuthKey 이런식으로 함
    //이런 명명법을 camelcase라고함
    //typescript에선 camelcase방식을 권장하지만 이것을 무시할 수도 있음(큰 문제는 없음)
    //이를 무시하기 위해 아래와 같이 주석을 달아줌
Singleton??
//싱글톤 패턴
//애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 인스턴스를 만들어 사용하는 디자인패턴.
//=> 싱글톤 패턴은 단 하나의 인스턴스를 생성해 사용하는 디자인 패턴이다.
//(인스턴스가 필요 할 때 똑같은 인스턴스를 만들어 내는 것이 아니라, 동일(기존) 인스턴스를 사용하게함)
//싱글톤 패턴을 쓰는 이유
//고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있음
//싱글톤 패턴의 문제점
//싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 "개방-폐쇄 원칙" 을 위배하게 된다. (=객체 지향 설계 원칙에 어긋남)
//따라서 수정이 어려워지고 테스트하기 어려워진다. 
//But, 적절히 사용하면 매우 유용하다.
//출처: https://jeong-pro.tistory.com/86 [기본기를 쌓는 정아마추어 코딩블로그]
동기 vs 비동기

동기(synchronous : 동시에 일어나는)
 - 동기는 말 그대로 동시에 일어난다는 뜻입니다. 
 - 요청과 그 결과가 동시에 일어난다는 약속인데요. 
 - 바로 요청을 하면 시간이 얼마가 걸리던지 요청한 자리에서 결과가 주어져야 합니다.
 - 요청과 결과가 한 자리에서 동시에 일어남
 - A노드와 B노드 사이의 작업 처리 단위(transaction)를 동시에 맞추겠다.
 
쉽게 이야기하면, 하나의 함수가 끝나고 반환 값을 받은 뒤 다음 함수를 실행하는 것.


비동기(Asynchronous : 동시에 일어나지 않는)
 - 비동기는 동시에 일어나지 않는다를 의미합니다. 
 - 요청과 결과가 동시에 일어나지 않을거라는 약속입니다. 
 - 요청한 그 자리에서 결과가 주어지지 않음
 - 노드 사이의 작업 처리 단위를 동시에 맞추지 않아도 된다.

쉽게 이야기하면 함수를 실행 시켜 놓고 다음 함수로 넘어가는 방식이다.


즉, 동기식=순차적 진행
비동기식=동시다발적 진행


거의 모든 html 구성을 JS로 만들고, 외부의 값들을 불러와 만들 때
일일히 외부의 값이 오기까지 기다리면 웹페이지의 로딩 속도가 굉장히 느려진다.
그럴 때 일단 모든 부분들을 표시해 놓고 비동기 방식으로 외부 값이 오면 그때 그때 
그 값을 페이지에 넣는 방식을 사용하면 웹페이지의 로딩이 빨라지게 된다.

출처: https://private.tistory.com/24 [공부해서 남 주자]
await??
- 비동기식 로직을 동기식으로 바꿔주는 함수?
- await을 쓰기 위해선 await이 달린 함수를 감싸고 있는 부모 함수에 
async를 붙여줘야 함

ex) async A(){
    await B(){
    }
}

# 주요 소스코드

<services/index.ts>

//service를 통해 mainAPI를 가져오는 방식으로 변경
//MVC패턴 같은 느낌
//import { MainService } from "@/types";
import { Member } from "@/types";
import { inject } from "vue";
import { getMainApi, MainApi } from "@/apis";  //service를 통해 mainAPI를 가져오는 방식으로 변경

  export class MainService {
    private mainApi: MainApi;
  
    constructor() {
      this.mainApi = getMainApi();
    }

    //camelcase??
    //일반적으로 객체 명명은 memberAuthKey 이런식으로 함
    //이런 명명법을 camelcase라고함
    //typescript에선 camelcase방식을 권장하지만 이것을 무시할 수도 있음(큰 문제는 없음)
    //이를 무시하기 위해 아래와 같이 주석을 달아줌
  
    /* eslint-disable @typescript-eslint/camelcase */
    member_authKey(loginId: string, loginPw: string) {
      return this.mainApi.member_authKey(loginId, loginPw);
    }

    /* eslint-disable @typescript-eslint/camelcase */
    article_list(boardId: number) {
      return this.mainApi.article_list(boardId);
    }


    // //이미지를 리사이징해주는 유틸 적용
    // //사용하려면 작동을 시켜야 함..일단은 적용 보류(21.04.01)
    // /* eslint-disable @typescript-eslint/no-inferrable-types */
    // getMemberThumbImgUrl(id: number, width: number = 40, height: number = 40) {

    //   const originUrl = 'http://localhost:8021/common/genFile/file/member/' + id + '/common/attachment/1';
    //   const url = `http://localhost:8085/img?failWidth=${width}&failHeight=${height}&failText=U.U&width=${width}&height=${height}&url=` + originUrl;
    //   return url;
    // }

    // /* eslint-disable @typescript-eslint/no-inferrable-types */
    // getArticleThumbImgUrl(id: number, width: number = 100, height: number = 100) {
    //   const originUrl = 'http://localhost:8021/common/genFile/file/article/' + id + '/common/attachment/1';
    //   const url = `http://localhost:8085/img?failWidth=${width}&failHeight=${height}&failText=U.U&width=${width}&height=${height}&url=` + originUrl;
    //   return url;
    //  }
  
    getMemberThumbImgUrl(id: number) {
      return "https://i.pravatar.cc/45?img=13&k=" + id
    }

    getArticleThumbImgUrl(id: number) {
      return "https://i.pravatar.cc/45?img=13&k=" + id
    }
  }
  
  export const mainServiceSymbol = Symbol('globalState');
  
  class Singleton {
    static mainService: MainService;
  }
  
  export const createMainService = () => {
    if ( Singleton.mainService == null ) {
      Singleton.mainService = new MainService();
    }
  
    return Singleton.mainService;
  };
  
  export const useMainService = (): MainService => inject(mainServiceSymbol) as MainService;

<stores/index.ts>

import { GlobalState } from '@/types'
import { reactive } from "@vue/reactivity"
import { inject, computed } from "vue"
import { Member } from "@/types";

//Symbol()
//'심볼(symbol)'은 유일한 식별자(unique identifier)를 만들고 싶을 때 사용합니다.
//자바스크립트는 객체 프로퍼티 키로 오직 문자형과 심볼형만을 허용합니다. 숫자형, 불린형 모두 불가능하고 오직 문자형과 심볼형만 가능하죠.
//Symbol()을 사용하면 심볼값을 만들 수 있습니다.
//심볼을 만들 때 심볼 이름이라 불리는 설명을 붙일 수도 있습니다.
//여기에서 심볼이릉은 'globalState'
//심볼은 유일성이 보장되는 자료형이기 때문에, 설명이 동일한 심볼을 여러 개 만들어도 각 심볼값은 다릅니다. 심볼에 붙이는 설명(심볼 이름)은 어떤 것에도 영향을 주지 않는 이름표 역할만을 합니다.
//설명 더보기 https://ko.javascript.info/symbol

export const globalStateSymbol = Symbol('globalState');

//Singleton??
//싱글톤 패턴
//애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 인스턴스를 만들어 사용하는 디자인패턴.
//=> 싱글톤 패턴은 단 하나의 인스턴스를 생성해 사용하는 디자인 패턴이다.
//(인스턴스가 필요 할 때 똑같은 인스턴스를 만들어 내는 것이 아니라, 동일(기존) 인스턴스를 사용하게함)
//싱글톤 패턴을 쓰는 이유
//고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있음
//싱글톤 패턴의 문제점
//싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 "개방-폐쇄 원칙" 을 위배하게 된다. (=객체 지향 설계 원칙에 어긋남)
//따라서 수정이 어려워지고 테스트하기 어려워진다. 
//But, 적절히 사용하면 매우 유용하다.
//출처: https://jeong-pro.tistory.com/86 [기본기를 쌓는 정아마추어 코딩블로그]

class Singleton{
  static globalState: GlobalState;
}


//전역적으로 사용할 것들 이곳에 등록
//그리고 types에서도 자료형? 등록 필요
//전역상태를 셋팅해놓는 이유는 여러 페이지에서 사용하기 위함
export const createGlobalState = () => {
  //만약, Singleton에 globalState가 없으면 다시 생성
  if( Singleton.globalState == null){
    const globalState: any = reactive({
      loginedMember: {
        id:0,
        regDate:"",
        updateDate:"",
        authLevel:0,
        cellphoneNo:"",
        email:"",
        /* eslint-disable @typescript-eslint/camelcase */
        extra__thumbImg:"",
        loginId:"",
        name:"",
        nickname:""
      },
      authKey: "",
      isLogined: computed(() => globalState.loginedMember.id != 0),
      setLogined: function(authKey: string, member: Member) {
        localStorage.setItem("authKey", authKey);
        localStorage.setItem("loginedMemberJsonStr", JSON.stringify(member));

        globalState.authKey = authKey;

        globalState.loginedMember = member;
      },
      setLogouted: function() {
        globalState.authKey = "";

        globalState.loginedMember.id = 0;
        globalState.loginedMember.regDate = "";
        globalState.loginedMember.updateDate = "";
        globalState.loginedMember.authLevel = 0;
        globalState.loginedMember.cellphoneNo = "";
        globalState.loginedMember.email = "";
        globalState.loginedMember.extra__thumbImg = "";
        globalState.loginedMember.loginId = "";
        globalState.loginedMember.name = "";
        globalState.loginedMember.nickname = "";

        localStorage.removeItem("authKey");
        localStorage.removeItem("loginedMemberJsonStr");
      }
    });
    const loadLoginInfoFromLocalStorage = () => {
      const authKey = localStorage.getItem("authKey");
      const loginedMemberJsonStr = localStorage.getItem("loginedMemberJsonStr");

      if ( !!authKey && !!loginedMemberJsonStr ) {
        const loginedMember: Member = JSON.parse(loginedMemberJsonStr);

        globalState.setLogined(authKey, loginedMember);
      }
    }

    // 이 함수는 브라우저를 열때(혹은 새로고침, F5키 누를 때)마다 1번씩 실행됨
    loadLoginInfoFromLocalStorage();

    Singleton.globalState = globalState;
  }
  
  return Singleton.globalState;
  
};

//useGlobalState 함수가 GlobalState 객체를 리턴한다
export const useGlobalState = (): GlobalState => inject(globalStateSymbol) as GlobalState;
//다른곳에서 createGlobalState라고 그대로 사용해도 크게 문제는 없음
//다만, 이해하기 쉽기 위해 useGlobalStateOnOutsideOfVue라고 명명해서 리턴하는 것
//그리고
//(): GlobalState => inject(globalStateSymbol) as GlobalState;와
//createGlobalState는 결국 같은 의미
export const getGlobalState = createGlobalState;

<Login.vue>

<template>
  <ion-page>
    <ion-custom-header>회원 - 로그인</ion-custom-header>
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">회원 - 로그인</ion-title>
        </ion-toolbar>
      </ion-header>

      <ion-custom-body class="justify-center">
        <div class="logo-box text-center">
          <span>
            <span class="text-3xl">
              <font-awesome-icon icon="lemon" />
            </span>
            <span class="font-bold text-3xl">
              DESIGN LEMON
            </span>
          </span>
        </div>
        <form @submit.prevent="checkAndLogin">
          <div>
            <ion-item>
              <ion-label position="floating">로그인아이디</ion-label>
              <ion-input v-model="loginFormState.loginId" maxlength="20"></ion-input>
            </ion-item>
          </div>
          <div>
            <ion-item>
              <ion-label position="floating">로그인비번</ion-label>
              <ion-input v-model="loginFormState.loginPw" maxlength="20" type="password"></ion-input>
            </ion-item>
          </div>
          <div class="py-2 px-4">
            <ion-button type="submit" expand="block">로그인</ion-button>
          </div>
          <div class="py-2 px-4">
            아직 회원이 아니신가요? <ion-custom-link to="/member/join">회원가입</ion-custom-link>
          </div>
        </form>
      </ion-custom-body>
    </ion-content>
  </ion-page>
</template>

<style>
</style>

<script lang="ts">
import { IonCustomHeader, IonCustomBody, IonCustomLink} from '@/components/';
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonLabel, IonInput, IonItem, IonButton } from '@ionic/vue';
import { useGlobalState } from '@/stores'
import { reactive } from 'vue';
//import { useMainApi } from '@/apis';  //mainService를 통해 mainAPI를 가져오는 방식으로 변경
import { useMainService } from '@/services';
import { useRouter } from 'vue-router';
import * as util from '@/utils';

const useLoginFormState = () => {
  return reactive({
    loginId: '',
    loginPw: '',
  })
}

export default  {
  name: 'Login',
  components: { IonHeader, IonToolbar, IonTitle, IonLabel, IonInput, IonItem, IonButton, IonContent, IonPage, IonCustomHeader, IonCustomBody, IonCustomLink },
  
  
  setup() {
    const globalState = useGlobalState();
    const loginFormState = useLoginFormState();
    const router = useRouter();
    //const mainApi = useMainApi();  //mainService를 통해 mainAPI를 가져오는 방식으로 변경
    const mainService = useMainService();
    
    
    //21.04.01
    //then() 방식에서 async-await 방식으로 변경

    // function login(loginId: string, loginPw: string) {
    //   mainService.member_authKey(loginId, loginPw) //mainService를 통해 mainAPI를 가져오는 방식으로 변경
    //     .then(axiosResponse => {

    //       //ionic alert으로 변경
    //       util.showAlert(axiosResponse.data.msg);

    //       if ( axiosResponse.data.fail ) {
    //         return;
    //       }
    //       const authKey = axiosResponse.data.body.authKey;
    //       const loginedMember = axiosResponse.data.body.member;

    //       globalState.setLogined(authKey, loginedMember);
          
    //       router.replace('/');
    //     });
    // }

    //await??
    //비동기식 로직을 동기식으로 바꿔주는 함수?
    //await을 쓰기 위해선 await이 달린 함수를 감싸고 있는 부모 함수에 async를 붙여줘야 함
    //기존 then()방식과 과정상 큰 차이는 없지만 아직 then의 개념은 익숙치 않아 await 방식으로 변경
    
    async function login(loginId: string, loginPw: string) {
      
      const axiosResponse = await mainService.member_authKey(loginId, loginPw)

      util.showAlert(axiosResponse.data.msg);
      if ( axiosResponse.data.fail ) {
        return;
      }
      const authKey = axiosResponse.data.body.authKey;
      const loginedMember = axiosResponse.data.body.member;
      globalState.setLogined(authKey, loginedMember);
      
      router.replace('/');
    }


    function checkAndLogin() {
      if ( loginFormState.loginId.trim().length == 0 ) {
        alert('아이디를 입력해주세요.');
        return;
      }
      if ( loginFormState.loginPw.trim().length == 0 ) {
        alert('비밀번호를 입력해주세요.');
        return;
      }
      login(loginFormState.loginId, loginFormState.loginPw);
    }



    return {
      globalState,
      loginFormState,
      checkAndLogin
    }
  }
}
</script> 

<List.vue>

<template>
  <ion-page>
    <ion-custom-header>게시물 - 리스트</ion-custom-header>
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">게시물 - 리스트</ion-title>
        </ion-toolbar>
      </ion-header>
      <ion-custom-body>
        <!-- Scrollable Segment -->
        <ion-segment scrollable :value="articleListState.boardId" @ionChange="changeBoardIdBySegment($event.detail.value);">
          <ion-segment-button value="1">
            공지사항
          </ion-segment-button>
          <ion-segment-button value="2">
            자유게시판
          </ion-segment-button>
        </ion-segment>

        <ion-list>
          <ion-list-header>
            {{articleListState.boardId == 1 ? '공지사항' : '자유'}} 게시물 리스트
          </ion-list-header>

          <ion-item v-for="article in articleListState.articles" :key="article.id">
            <ion-avatar slot="start">
              <img :src="mainService.getMemberThumbImgUrl(article.memberId)">
            </ion-avatar>
            <ion-label>
              <h2>{{ article.title }}</h2>
              <h2>{{ article.extra__writer }}</h2>
              <h2>{{ article.body }}</h2>
            </ion-label>
          </ion-item>
        </ion-list>
      </ion-custom-body>
    </ion-content>
  </ion-page>
</template>

<style>
</style>

<script lang="ts">
import { IonCustomBody, IonCustomHeader } from '@/components/';
import { IonLabel, IonAvatar, IonPage, IonHeader, IonToolbar, IonTitle, IonListHeader, IonList, IonItem, IonContent, IonSegment, IonSegmentButton } from '@ionic/vue';
import { useGlobalState } from '@/stores'
import { useMainService } from '@/services';
import { reactive, watch } from 'vue';
import { Article } from '@/types';
import { useRoute, useRouter } from 'vue-router';
import * as util from '@/utils';



export default  {
  name: 'List',
  
  components: { IonLabel, IonAvatar, IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonCustomBody, IonCustomHeader, IonSegment, IonSegmentButton, IonListHeader, IonList, IonItem },
  
  setup() {
    const route = useRoute();
    const router = useRouter();
    const globalState = useGlobalState();
    const mainService = useMainService();

    const articleListState = reactive({
      articles: ([] as Article[]),
      boardId: 0
    });

    //(1) route에 들어있는 boardId값을 가져온다(없으면 1로 치환)
    const boardIdInQuery = util.toInt(route.query.boardId, 1);

    //(3)
    async function loadArticles(boardId: number) {
      // articleListState의 boardId값을 들어온 값으로 바꿔준다.
      articleListState.boardId = boardId;

      // mainService를 통해 axiosResponse 요청하고 받는다
      const axRes = await mainService.article_list(boardId);

      // axiosResponse으로 받은 articles를 articleListState의 articles로 바꿔준다.
      articleListState.articles = axRes.data.body.articles;
    }

    //(4) 항상 route.query값을 모니터링하면서 boardId값이 바뀌면 (2),(3)번을 수행
    watch(() => route.query, () => {
      loadArticles(util.toInt(route.query.boardId, 1));
    })

    //(2) boardIdInQuery 값을 받아 loadArticles 함수 실행
    loadArticles(boardIdInQuery);


    //ion-segment-button 값의 따라 router값이 바뀜
    function changeBoardIdBySegment(boardId: number) {
      router.push('/article/list?boardId=' + boardId);
    }

    return {
      globalState,
      articleListState,
      mainService,
      changeBoardIdBySegment
    }
  }
}
</script>