BLIND CLONE CODING 3일차 - Nuxt FrontEnd
Main Page
Searchbar.vue
일단 위와같이 만들었다. 가급적 모두 만들고 시작하자
사실 상태가 문제다. 항상 문제다.
상태를 페이지단위로 처리하거나 기능단위로 처리하거나 등등
이 상황에서는 페이지 결과에 따라서 처리해주는 것도 좋을거 같다.
암튼 우선은 db와 직접 통신할 store부분은 모두 state, mutation, actions들을 정의해놓자
전역에서 접근할 수 있는 것을 modal.js 라고 해서 새로 만들어주자
이제 로그인 관리를 위해 modal을 만들어주자
이렇게 하면 로그인 모달이 켜진다.
이게 아마 auto Import 켜져있다고 디렉토리가 모두 켜져있는건 아니다.
위와같이 GNB만 import 해서 GNB 내부에서 모달을 관리해주자
GNB는 네비게이션으로 쓰자. 일단 필요한 것들을 Modal 디렉토리에서 구해왔다.
VS Code에서 .Vue 자동완성(Emmet)기능이 작동하지 않을 때 - developer irostub
VS Code에서 .Vue 자동완성(Emmet)기능이 작동하지 않을 때
.Vue 에서 HTML의 자동완성 기능(Emmet)을 사용
irostub.github.io
<template>
<div v-if="modal.login.show" class="modal-outside">
<div id="login-modal">
<div class="head">
<h5>Alumni 로그인</h5>
<a
@click.prevent="$store.commit('modal/SET_LOGIN_MODAL_CLOSE')"
class="close-btn"
>
<img src="" alt="" />
</a>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
computed: { ...mapState(["modal"]) },
};
</script>
<style lang="scss"></style>
Login.vue
//개별 게시글의 상태
export const state = () => ({
login: { show: false, directLogin: false },
});
export const mutations = {
SET_LOGIN_MODAL_OPEN(state) {
state.login.show = true;
},
SET_LOGIN_MODAL_DIRECT_LOGIN(state) {
state.login.directLogin = true;
},
SET_LOGIN_MODAL_CLOSE(state) {
state.login = {
show: false,
directLogin: false,
};
},
};
export const actions = {};
/store/modal.js 파일이다. 이를 통해 알 수 있는 것
1. state는 객체형태의 변수들이다.
2. mutations는 state의 상태를 바꾸는 함수들이다.
3. actions는 단순 상태가 아닌 복잡한 기능이다.
이렇게 하는 이유는 React의 Redux와 같이 외부 라이브러리를 통해서 데이터를 관리하지 않기 위함이다. 단순하게 위와같이 작성해주는 것만으로도 전역에서 사용할 수 있는 상태가 저장되고 사용될 수 있다.
<template>
<nav>
<div class="left-side">
<nuxt-link to="/">
<img src="/logo/main.jpg" alt="알럼나이 로고" />
</nuxt-link>
<nuxt-link to="/"> 홈 </nuxt-link>
<nuxt-link to="/company"> 기업리뷰 </nuxt-link>
</div>
<div class="right-side">
<SmallSearchbar />
<a @click.prevent="clickWritingButton"> 글쓰기 </a>
<a @click.prevent="clickLoginButton"> 로그인 </a>
</div>
<LoginModal />
<WritingModal />
</nav>
</template>
<script>
import SmallSearchbar from "@/components/GNB/SmallSearchbar";
import LoginModal from "@/components/Modal/Login";
import WritingModal from "@/components/Modal/Writing";
import { mapState } from "vuex";
export default {
components: {
SmallSearchbar,
LoginModal,
WritingModal,
},
computed: {
...mapState(["user"]),
},
methods: {
clickWritingButton() {
if (!this.user.email) {
this.$store.commit("modal/SET_LOGIN_MODAL_OPEN");
}
},
clickLoginButton() {
if (!this.user.email) {
this.$store.commit("modal/SET_LOGIN_MODAL_OPEN");
}
},
},
};
</script>
<style lang=""></style>
/components/GNB.vue 이다. 크게 구조가 3개인것을 확인할 수 있다.
1. 화면에 표현할 부분을 나타내는 template 부분
2. 어떤 외부라이브러리, 컴 포넌트를 가져다 쓸지를 나타내는 script 부분
3. 화면을 꾸미는 style 부분이다.
보면 nav 태그 안에서도 태그들을 통해 나누어져 있다. 그리고 className이 아닌 그냥 class를 쓰나보다.
nuxt-link라는 것도 보이는데 이는 React-Router-Dom의 Link to와 같은 기능인거 같다. 즉 페이지를 전체 바꾼느게 아닌 컴포넌트만 바꾸는 기능인 것으로 보인다.
<nuxt-link to="/"></nuxt-link>
컴포넌트 사용은 리액트와 동일하다.
Vue.js의 이벤트 사용 방법 정리 (tistory.com)
Vue.js의 이벤트 사용 방법 정리
Vue.js에서 이벤트를 다루는 방법에 대해서 간단히 정리하려고 합니다. HTML에는 나 처럼 당연히 알고 있는 기본 이벤트가 있습니다. 그리고 이미 javascript 를 통해서 기본 이벤트와 어우러지게 사
ux.stories.pe.kr
vue의 이벤트를 사용하기 위해서는 v-on.click과 같은 문법을 사용해야하나보다. 하지만 v-on을 없애기 위해서 `@`를 사용하는 것도 하나의 방법인걸로 보인다.
이거외에도 javascript 에서 사용하고 있던 기능들을 보조하기 위한 속성들이 추가됨
위와같이 수식어가 있고 현재 쓰인 prevent는 preventDefault와 동일하게 기본 이벤트의 자동 실행을 중단시킨다.
그 다음 script 부분이다. 여기에서는 성분을 가져오기 위해 import SmallSearchbar from "@/components/GNB/smallSearchbar"이라는 문법을 사용했다.
`@`는 nuxt에서 사전에 설정한 baseURL인 `/`을 타고 온걸로 보인다.
computed
[Vue-18]vuex에서 helper를 사용하기(mapState, mapMutations, mapActions, mapGetters)
JavaScript와 HTML, CSS등에 대해서는 일체 다루지 않는다. 기초지식은 다른 강의를 참조하도록 하라. 참고: [Package]Bower(Front End Pacakage 관리) 설치 [Package]Vue.js https://vuex.vuejs.org/kr/ http://v..
kamang-it.tistory.com
mapState는 vuex helper로 redux의 기능을 수행한다. computed에 반드시 써주어야 하는 것은 아니지만 권장된다. 이렇게 쓰면 user.js에 선언된 state를 가져와서 마음대로 사용이 가능해진다.
this.$store.state.user를 줄이기 위해서 사용한다고 봐도 된다.
methods
vue 컴포넌트에서 사용할 함수를 정의하는 부분
여기에서 this.user.email은 짜피 그냥 내가 computed에서 가져온 상태를 확인하는건 확실한데
this.$store.commit은 공부해봐야할듯
---
<template>
<div v-if="modal.login.show" class="modal-outside">
<div id="login-modal">
<div class="head">
<h5>Alumni 로그인</h5>
<a
@click.prevent="$store.commit('modal/SET_LOGIN_MODAL_CLOSE')"
class="close-btn"
>
<img src="" alt="" />
</a>
</div>
<div class="body">
<p>
Alumni 에서는 <b>(취업자/졸업생/이너서클 완료자)</b>분들과
<b>카뎃분들</b> 그리고 <b>42서울 내 프로젝트</b>들을 이어주는
커뮤니티로 성장해 나갈겁니다.
</p>
<div class="info">42서울 로그인</div>
<button class="seoul42-btn">42Seoul 계정으로 로그인하기</button>
<div class="left-time">남은시간 : {{ displayTime }}</div>
</div>
<div class="foot">
<a @click.prevent="">알럼나이는 처음이신가요?</a>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
computed: { ...mapState(["modal"]) },
data() {
return {
leftTime: 180,
displayTime: "3분",
};
},
created() {
setInterval(() => {
this.timeModifier();
}, 1000);
},
methods: {
timeModifier() {
this.leftTime -= 1;
if (this.leftTime <= 0) {
this.leftTime = 180;
this.displayTime = `3분 ${this.displayTime}`;
} else if (this.leftTime >= 120) {
this.displayTime = `2분 ${this.leftTime - 120}초`;
} else if (this.leftTime >= 60) {
this.displayTime = `1분 ${this.leftTime - 60}초`;
} else {
this.displayTime = `${this.leftTime}초`;
}
},
},
};
</script>
<style lang="scss" scoped>
#login-modal {
background: white;
width: 520px;
.head {
padding: 23px 30px;
font-size: 18px;
color: rgb(34, 34, 34);
font-weight: 700;
border-bottom: solid 1px rgb(223, 225, 228);
h5 {
margin: 0;
}
}
.body {
padding: 0 30px;
font-size: 16px;
line-height: 24px;
p {
padding: 20px 0;
margin: 0;
}
.info {
margin: 20px 0 30px;
color: rgb(148, 150, 155);
font-size: 14px;
line-height: 21px;
letter-spacing: -0.1px;
}
.seoul42-btn {
display: flex;
justify-content: center;
align-items: center;
font-size: 25px;
font-weight: 400;
width: 100%;
height: 65px;
background-color: rgb(55, 172, 201);
border: none;
color: white;
cursor: pointer;
}
.left-time {
text-align: center;
color: rgb(55, 172, 201);
font-size: 14px;
font-weight: 700;
margin-top: 16px;
}
}
.body {
}
.foot {
padding: 30px;
color: rgb(160, 160, 174);
font-size: 14px;
line-height: 17.5px;
text-align: center;
text-decoration: underline;
}
}
</style>
```
yarn add node-sass sass-loader
yarn add @nuxtjs/style-resources
```
click 이벤트를 a에 넣는 이유는 div는 근본적으로 container을 감싸기 때문. 기능을 할때는 a를 사용해서 기능상의 통일성을 부여하는 것이다.
```
npx yarn add @nuxtjs/moment
```
---
지금 문제가 시간이 그냥 화면이 켜지면 돈다. 이걸 watch로 잡아주자
<template>
<div v-if="modal.login.show" class="modal-outside">
<div id="login-modal">
<div class="head">
<h5>{{ modal.login.directLogin ? "로그인" : "Alumni 로그인" }}</h5>
<a
@click.prevent="$store.commit('modal/SET_LOGIN_MODAL_CLOSE')"
class="close-btn"
>
<img src="" alt="" />
</a>
</div>
<div v-if="!modal.login.directLogin" class="body">
<p>
Alumni 에서는 <b>(취업자/졸업생/이너서클 완료자)</b>분들과
<b>카뎃분들</b> 그리고 <b>42서울 내 프로젝트</b>들을 이어주는
커뮤니티로 성장해 나갈겁니다.
</p>
<div class="info">42서울 로그인</div>
<button class="seoul42-btn">42Seoul 계정으로 로그인하기</button>
<div class="left-time">남은시간 : {{ displayTime }}</div>
</div>
<div v-else class="body">
<div class="row">
<label for="user-email">이메일</label>
<input id="user-email" type="email" v-model="email" />
</div>
<div class="row">
<label for="user-password">비밀번호</label>
<input id="user-password" type="password" v-model="password" />
</div>
</div>
<div v-if="modal.login.show" class="foot">
<a @click.prevent="$store.commit('modal/SET_LOGIN_MODAL_DIRECT_LOGIN')"
>알럼나이는 처음이신가요?</a
>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
computed: { ...mapState(["modal"]) },
data() {
return {
leftTime: 180,
displayTime: "3분",
email: null,
password: null,
};
},
watch: {
"modal.login.show": function (to, from) {
if (to !== from && to) {
this.leftTime = 180;
setInterval(() => {
this.timeModifier();
}, 1000);
}
},
},
methods: {
timeModifier() {
this.leftTime -= 1;
if (this.leftTime <= 0) {
this.leftTime = 180;
this.displayTime = `3분 ${this.displayTime}`;
} else if (this.leftTime >= 120) {
this.displayTime = `2분 ${this.leftTime - 120}초`;
} else if (this.leftTime >= 60) {
this.displayTime = `1분 ${this.leftTime - 60}초`;
} else {
this.displayTime = `${this.leftTime}초`;
}
},
},
};
</script>
<style lang="scss" scoped>
#login-modal {
background: white;
width: 520px;
.head {
padding: 23px 30px;
font-size: 18px;
color: rgb(34, 34, 34);
font-weight: 700;
border-bottom: solid 1px rgb(223, 225, 228);
h5 {
margin: 0;
}
}
.body {
padding: 0 30px;
font-size: 16px;
line-height: 24px;
p {
padding: 20px 0;
margin: 0;
}
.info {
margin: 20px 0 30px;
color: rgb(148, 150, 155);
font-size: 14px;
line-height: 21px;
letter-spacing: -0.1px;
}
.seoul42-btn {
display: flex;
justify-content: center;
align-items: center;
font-size: 25px;
font-weight: 400;
width: 100%;
height: 65px;
background-color: rgb(55, 172, 201);
border: none;
color: white;
cursor: pointer;
}
.left-time {
text-align: center;
color: rgb(55, 172, 201);
font-size: 14px;
font-weight: 700;
margin-top: 16px;
}
}
.body {
}
.foot {
padding: 30px;
color: rgb(160, 160, 174);
font-size: 14px;
line-height: 17.5px;
text-align: center;
text-decoration: underline;
}
}
</style>
백과 통신해서 로그인도 해보자
그냥 User.find 로 되어 있었는데 이러면 배열로 반환해준다. 그래서 하나만 반환해주도록 바꾸어야한다.
그리고 성공시에 error 문제가 뜰 수 있으니 성공시에도 형태가 동일하게 보내주자.
<template>
<div v-if="modal.login.show" class="modal-outside">
<div id="login-modal">
<div class="head">
<h5>{{ modal.login.directLogin ? "로그인" : "Alumni 로그인" }}</h5>
<a
@click.prevent="$store.commit('modal/SET_LOGIN_MODAL_CLOSE')"
class="close-btn"
>
<img src="@/static/icon/close.png" alt="닫기" />
</a>
</div>
<div v-if="!modal.login.directLogin" class="body">
<p>
Alumni 에서는 <b>(취업자/졸업생/이너서클 완료자)</b>분들과
<b>카뎃분들</b> 그리고 <b>42서울 내 프로젝트</b>들을 이어주는
커뮤니티로 성장해 나갑니다.
</p>
<div class="info">42서울 로그인</div>
<button class="seoul42-btn">42Seoul 계정으로 로그인하기</button>
<div class="left-time">남은시간 : {{ displayTime }}</div>
</div>
<div v-else class="body">
<div class="row">
<label for="user-email">이메일</label>
<input id="user-email" type="email" v-model="email" />
</div>
<div class="row">
<label for="user-password">비밀번호</label>
<input id="user-password" type="password" v-model="password" />
</div>
<button class="login-btn" @click="loginWithEmail">
이메일로 로그인
</button>
</div>
<div v-if="!modal.login.directLogin" class="foot">
<a @click.prevent="$store.commit('modal/SET_LOGIN_MODAL_DIRECT_LOGIN')"
>알럼나이는 처음이신가요?</a
>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
computed: { ...mapState(["modal"]) },
data() {
return {
leftTime: 180,
displayTime: "3분",
email: null,
password: null,
};
},
watch: {
"modal.login.show": function (to, from) {
if (to !== from && to) {
this.leftTime = 180;
setInterval(() => {
this.timeModifier();
}, 1000);
}
},
},
methods: {
async loginWithEmail() {
const data = await this.$axios.$post(`http://localhost:8000/user/login`, {
email: this.email,
password: this.password,
});
// 로그인 에러 캐칭
if (data.error) {
return;
}
this.$store.commit("user/SET_USER", {
email: data.email,
nickname: data.nickname,
});
this.$store.commit("modal/SET_LOGIN_MODAL_CLOSE");
},
timeModifier() {
this.leftTime -= 1;
if (this.leftTime <= 0) {
this.leftTime = 180;
this.displayTime = `3분 ${this.displayTime}`;
} else if (this.leftTime >= 120) {
this.displayTime = `2분 ${this.leftTime - 120}초`;
} else if (this.leftTime >= 60) {
this.displayTime = `1분 ${this.leftTime - 60}초`;
} else {
this.displayTime = `${this.leftTime}초`;
}
},
},
};
</script>
<style lang="scss" scoped>
#login-modal {
background: white;
width: 520px;
.head {
padding: 23px 30px;
font-size: 18px;
color: rgb(34, 34, 34);
font-weight: 700;
border-bottom: solid 1px rgb(223, 225, 228);
h5 {
margin: 0;
}
}
.body {
padding: 0 30px;
font-size: 16px;
line-height: 24px;
p {
padding: 20px 0;
margin: 0;
}
.info {
margin: 20px 0 30px;
color: rgb(148, 150, 155);
font-size: 14px;
line-height: 21px;
letter-spacing: -0.1px;
}
.row {
margin: 20px 0;
label {
display: block;
}
input {
width: 100%;
box-sizing: border-box;
padding: 12px;
}
}
.seoul42-btn {
display: flex;
justify-content: center;
align-items: center;
font-size: 25px;
font-weight: 400;
width: 100%;
height: 65px;
background-color: rgb(55, 172, 201);
border: none;
color: white;
cursor: pointer;
}
.login-btn {
display: flex;
justify-content: center;
align-items: center;
font-size: 25px;
font-weight: 400;
width: 100%;
height: 64px;
background-color: rgb(55, 172, 201);
border: none;
color: white;
cursor: pointer;
margin-bottom: 30px;
}
.left-time {
text-align: center;
color: rgb(55, 172, 201);
font-size: 14px;
font-weight: 700;
margin-top: 16px;
}
}
.body {
}
.foot {
padding: 30px;
color: rgb(160, 160, 174);
font-size: 14px;
line-height: 17.5px;
text-align: center;
text-decoration: underline;
}
}
</style>
<template>
<div class="nav-container">
<nav>
<div class="side-block">
<nuxt-link to="/" id="logo-btn">
<img src="/logo/main.jpg" alt="알럼나이 로고" />
</nuxt-link>
<nuxt-link to="/" class="text-menu"> 홈 </nuxt-link>
<nuxt-link to="/company" class="text-menu"> 기업리뷰 </nuxt-link>
</div>
<div class="side-block">
<SmallSearchbar />
<a @click.prevent="clickWritingButton" id="write-btn"> 글쓰기 </a>
<a @click.prevent="clickLoginButton" id="login-btn">
{{ user.email ? "로그아웃" : "로그인" }}
</a>
</div>
<LoginModal />
<WritingModal />
</nav>
</div>
</template>
<script>
import SmallSearchbar from "@/components/GNB/SmallSearchbar";
import LoginModal from "@/components/Modal/Login";
import WritingModal from "@/components/Modal/Writing";
import { mapState } from "vuex";
export default {
components: {
SmallSearchbar,
LoginModal,
WritingModal,
},
computed: {
...mapState(["user"]),
},
methods: {
clickWritingButton() {
if (!this.user.email) {
this.$store.commit("modal/SET_LOGIN_MODAL_OPEN");
}
},
clickLoginButton() {
if (!this.user.email) {
this.$store.commit("modal/SET_LOGIN_MODAL_OPEN");
}
},
},
};
</script>
<style lang="scss" scoped>
.nav-container {
border-bottom: 1px solid #d4d4d4;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 80px;
margin: auto;
padding: 0 20px;
max-width: 1100px;
.side-block {
display: flex;
height: 100%;
align-items: center;
#logo-btn {
margin-right: 60px;
}
.text-menu {
color: #222;
font-size: 20px;
margin-right: 30px;
}
#write-btn {
background: rgb(218, 50, 56);
color: white;
font-size: 14px;
height: 40px;
margin-left: 10px;
width: 82px;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
#login-btn {
background: white;
color: rgb(34, 34, 34);
font-size: 14px;
border: solid 1px rgb(212, 212, 212);
height: 40px;
margin-left: 10px;
width: 82px;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
}
}
</style>
GNB.vue
<template>
<div class="wrap-search">
<img class="search-icon" src="/icon/search.png" alt="검색" />
<input type="text" />
</div>
</template>
<script>
export default {};
</script>
<style lang="scss" scoped>
.wrap-search {
width: 200px;
margin-right: 8px;
input {
width: 100%;
height: 40px;
padding: 0 10px 0 41px;
border: 1px solid #d4d4d4;
border-radius: 20px;
font-size: 14px;
box-sizing: border-box;
}
.search-icon {
position: absolute;
margin-top: 10px;
margin-left: 17px;
text-indent: -9999px;
opacity: 0.4;
width: 20px;
height: 20px;
}
}
</style>
/GNB/SmallSearchbar.vue