21.04.01 lamplight서비스 프로젝트(ionic 기반으로 옮기기 위한 ionic exam template 진행(tailwind, fontawesome, 로그인,게시물리스팅 가능))
2021. 4. 1. 22:08ㆍVue.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>
'Vue.js > Spring & Vue APP 프로젝트(프론트엔드)' 카테고리의 다른 글
21.04.03~04.06 lamplight서비스 프로젝트(ionic이사 진행중-의뢰인로그인,회원가입,정보수정.....지도사리스팅....요청서crud, 리스팅 완료) (0) | 2021.04.06 |
---|---|
21.04.02 lamplight서비스 프로젝트(멤버쪼개기 완료, funeral 테이블 추가, 요청수락, 진행단계별 버튼 변경 기능 구현까지) (0) | 2021.04.02 |
21.03.29 lamplight서비스 프로젝트(Member 쪼개기 작업 진행중) (0) | 2021.03.29 |
21.03.27 ionic 전역상태 세팅, 각 페이지 생성, 하단 메뉴 연결 등 (0) | 2021.03.27 |
21.03.25 ionic 프레임워크 초기 셋팅 (0) | 2021.03.25 |