0426 - 0502
0426
게시글 작성 페이지 구현
게시글 작성 페이지는 상단의 header와 게시글 제목과 내용을 작성할 form으로 나누어서 구현하였다.
이를 구현하면서 고민했던 내용 중 하나는 서로 다른 컴포넌트에서 어떻게 작성 버튼을 눌렀을 때, form에 있는 내용을 보낼까 하는 것이었다.
<!-- PostHeader.vue -->
<template>
<div class="title_mainTop">
<div class="title_leftNav">
<button class="btn_basic" @click="addPostMethod">작성</button>
<button class="btn_basic" @click="goMain">취소</button>
</div>
<div class="title_center">
<span @click="goMain">Main</span>
</div>
<div class="title_rightNav">
<ul>
<li><img class="rightNav_profile" /></li>
<li>
<span title="사용자정보">님</span>
</li>
<li><button @click="logoutMethod">로그아웃</button></li>
</ul>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from "vuex";
export default {
computed: {
...mapState({
memberName: (state) => state.member.memberName,
}),
},
methods: {
...mapMutations(["logout"]),
logoutMethod() {
const result = confirm(
"사이트에서 나가시겠습니까?\n변경사항이 저장되지 않을 수 있습니다."
);
if (result) {
this.logout();
this.$router.push("/login");
}
},
addPostMethod() {
this.$emit("addPost");
},
goMain() {
const result = confirm(
"사이트에서 나가시겠습니까?\n변경사항이 저장되지 않을 수 있습니다."
);
if (result) {
this.$router.push("/main");
}
},
},
};
</script>
<style scoped src="../../assets/css/post/header.css"></style>
- 페이지를 이동하는 메소드가 실행되었을 경우에는 confirm으로 확인함으로써 의도치 않게 작성하던 글이 날라가지 않도록 설정하였다.
- PostHeader와 PostForm은 모두 Post의 자식 컴포넌트이므로 작성 버튼 클릭 시 $emit 이벤트를 전달하였다.
<!-- Post.vue -->
<template>
<div>
<PostHeader v-on:addPost="addPost"></PostHeader>
<PostForm ref="postForm"></PostForm>
</div>
</template>
<script>
import PostHeader from "@/components/post/PostHeader.vue";
import PostForm from "@/components/post/PostForm.vue";
export default {
components: {
PostHeader,
PostForm,
},
methods: {
addPost() {
this.$refs.postForm.addPostMethod();
},
},
};
</script>
- addPost를 이벤트로 받아서 PostForm을 ref로 지정하여 자식 컴포넌트에 있는 addPostMethod 메소드를 실행시켰다.
<!-- PostForm.vue -->
<template>
<div class="content_mainBox">
<div class="content_tableArea02">
<table class="content_mainTable02">
<colgroup>
<col style="width: 5%;" />
<col />
</colgroup>
<tr>
<th>제목</th>
<td>
<input
type="text"
class="board_write"
placeholder="제목을 입력해 주세요."
v-model="title"
/>
</td>
</tr>
</table>
</div>
<div class="content_mainCont">
<textarea placeholder="내용을 입력하세요." v-model="content"></textarea>
</div>
</div>
</template>
<script>
import { mapActions } from "vuex";
export default {
data() {
return {
title: "",
content: "",
};
},
methods: {
...mapActions(["addPost"]),
async addPostMethod() {
try {
const post = {
title: this.title,
content: this.content,
};
const result = await this.addPost(post);
console.log(result);
this.$router.push("/main");
} catch (error) {
console.log(error.response);
alert(error.response.data.message);
}
},
},
};
</script>
- 부모 컴포넌트로부터 메소드를 실행시켜서 vuex의 action 메소드를 통해 title과 content를 전달해서 게시글을 추가하는 기능을 완성시켰다.
0427
CORS 문제 해결
클라이언트 단에서 Authorization 헤더에 토큰 값을 넣어 보내는 구현 방식을 선택하였는데, 계속되는 CORS 문제로 인해서 정상 작동 하지 않았다.
// WebConfiguration.java
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(baseHandlerInterceptor());
registry.addInterceptor(bearerAuthInterceptor())
.excludePathPatterns("/members/*");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// 모든 uri에 대해 http://localhost:3000의 접근을 허용한다.
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowCredentials(true);
}
}
- WebConfiguration은 CORS와 관련된 로직 및 인터셉터를 관리하는 클래스인데, 기본적인 설정을 해뒀음에도 불구하고 CORS 문제가 발생하는 데는 특별한 이유가 있다는 생각이 들었다.
- bearerAuthInterceptor는 토큰과 관련된 로직을 담당하는 인터셉터 영역인데, 인증이 필요없는 /members로 호출하는 api만 제외시키고 모두 이 영역을 거쳐야 한다.
- 검색을 하면서 여러 글들을 읽어보고 시도해봤지만 되지 않는 것으로 봐서는 bearerAuthInterceptor를 거치는 내부 로직에서 문제가 있다고 판단하였다.
- OPTIONS 예비 메소드가 넘어올 때도 인터셉터를 거쳐서 에러가 발생하는 것이 아닐까? 하고 생각하여 OPTIONS 메서드의 경우 true 값을 반환하도록 설정하니 제대로 동작하였다.
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getMethod().equals("OPTIONS")) {
log.info("option 메서드인 경우, true 반환");
return true;
}
}
0428
페이징 처리
로그인을 하면 첫 화면에 존재하는 게시글의 목록이 나타나는데, 10개 씩 묶어서 페이징 처리를 하기 위한 작업을 했다.
Pagination.vue 작성
페이징 처리를 할 때 사용할 부분을 컴포넌트화 했다.
<template>
<div class="pagiNation">
<ul>
<li>
<a v-if="prev" @click="changeLink(startPageIndex - 1)">prev</a>
</li>
<li v-for="index in endPageIndex - startPageIndex + 1" :key="index">
<a
:class="{ on: startPageIndex + index - 1 == currentPageNo }"
@click="changePage(index)"
>
startPageIndex + index - 1
</a>
</li>
<li>
<a v-if="next" @click="changeLink(endPageIndex + 1)">next</a>
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["currentPageNo", "totalCnt", "recordsPerPage", "pagePerLink"], // totalCnt: 총 리스트 개수, recordsPerPage: 한 페이지당 리스트 개수, pagePerLink: 링크당 페이지 개수
data() {
return {
pageCount: 0, //페이지 개수
startPageIndex: 0, //링크에서 페이지 시작 인덱스
endPageIndex: 0, //링크에서 페이지 마지막 인덱스
prev: false,
next: false,
};
},
methods: {
initUI() {
this.pageCount = Math.ceil(this.totalCnt / this.recordsPerPage);
if (this.currentPageNo % this.pagePerLink == 0) {
this.startPageIndex =
(this.currentPageNo / this.pagePerLink - 1) * this.pagePerLink + 1;
} else {
this.startPageIndex =
Math.floor(this.currentPageNo / this.pagePerLink) * this.pagePerLink +
1;
}
if (this.currentPageNo % this.pagePerLink == 0) {
this.endPageIndex =
(this.currentPageNo / this.pagePerLink - 1) * this.pagePerLink +
this.pagePerLink;
} else {
this.endPageIndex =
Math.floor(this.currentPageNo / this.pagePerLink) * this.pagePerLink +
this.pagePerLink;
}
if (this.currentPageNo <= this.pagePerLink) {
this.prev = false;
} else {
this.prev = true;
}
if (this.endPageIndex >= this.pageCount) {
this.endPageIndex = this.pageCount;
this.next = false;
} else {
this.next = true;
}
},
changePage(index) {
let selectedPage = this.startPageIndex + index - 1;
this.$emit("update", selectedPage);
},
changeLink(index) {
this.$emit("update", index);
},
},
watch: {
currentPageNo: function () {
this.initUI();
},
totalCnt: function () {
this.initUI();
},
recordsPerPage: function () {
this.initUI();
},
pagePerLink: function () {
this.initUI();
},
},
created() {
if (this.currentPageNo !== undefined) {
this.initUI();
}
},
};
</script>
<style scoped src="../../assets/css/board/pagination.css"></style>
- 기본적으로 내가 구성한 Pagination은 한 링크당 5페이지로 구성되어 있고, 한 페이지당 10개의 게시글로 구성되어 있다.
- next 버튼을 누르면 다음 링크로 넘어가서 첫 페이지를 보여주고, prev 버튼을 누르면 이전 링크로 넘어가서 마지막 페이지를 보여주게 설정하였다. 다만 각 버튼은 다음이나 이전 링크에 페이지가 존재해야지만 활성화 된다.
- Pagination은 부모 컴포넌트로부터 현재 페이지 번호, 총 게시글 수, 링크당 페이지 수, 페이지당 게시글 수 데이터를 넘겨받아서 그 값을 토대로 initUI 메소드를 실행시켜 값을 계산한다.
- 다른 페이지를 클릭하거나 링크를 이동하면 인덱스를 이벤트로 넘겨서 부모 컴포넌트의 메소드를 실행시킨다.
- 부모 컴포넌트에서 메소드를 실행시켜 해당 페이지에 대한 결과를 반환하면, watch 속성을 통해 initUI 메소드를 다시 실행시켜 Pagination이 재정의된다.
0429
검색 기능과 페이징 처리
작성했던 Pagination을 이용하여 검색 기능과 검색을 통한 결과를 페이징 처리하는 기능을 구현하였다.
기본적으로 로그인하면 전체 결과에 대한 리스트를 보여주고, 검색창에 검색 시에만 해당하는 검색어가 게시글 제목이나 내용에 속해있는 결과를 페이징 처리하여 반환해주는 형태로 구현하였다.
<template>
<div class="content_mainBox">
<div class="content_search">
<input
type="text"
class="login_id"
placeholder="검색어를 입력해주세요"
v-model="searchWord"
@keyup.enter="getBoardListMethod"
/>
<button class="searchBtn" title="search" @click="getBoardListMethod">
검색
</button>
</div>
<div class="result_total">검색결과 건</div>
<div class="content_tableArea">
<table class="content_mainTable">
<colgroup>
<col style="width: 10%;" />
<col />
<col style="width: 15%;" />
<col style="width: 15%;" />
<col style="width: 10%;" />
</colgroup>
<tr>
<th>No</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>좋아요</th>
</tr>
<tr v-for="(board, index) in boardList.list" v-bind:key="index">
<td></td>
<td>
<a href=""></a>
</td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
<div class="no_result" v-if="boardList.totalCnt == 0">
검색 결과가 없습니다.
</div>
<pagination
@update="changePage"
v-bind:currentPageNo="boardList.currentPageNo"
v-bind:totalCnt="boardList.totalCnt"
v-bind:recordsPerPage="boardList.recordsPerPage"
v-bind:pagePerLink="5"
>
</pagination>
</div>
</template>
<script>
import { mapActions } from "vuex";
import Pagination from "./Pagination";
export default {
data() {
return {
boardList: {},
currentPageNo: 1,
searchWord: "",
};
},
methods: {
...mapActions(["getBoardList", "getBoardSearchList"]),
async getBoardListMethod() {
try {
const param = {
searchWord: this.searchWord.trim(),
currentPageNo: 1,
};
if (param.searchWord === "") {
this.boardList = await this.getBoardList(param);
console.log(this.boardList);
} else {
this.boardList = await this.getBoardSearchList(param);
console.log(this.boardList);
}
} catch (error) {
console.log(error.response);
}
},
async changePage(pageNo) {
try {
this.currentPageNo = pageNo;
const param = {
searchWord: this.searchWord,
currentPageNo: this.currentPageNo,
};
if (param.searchWord === "") {
this.boardList = await this.getBoardList(param);
console.log(this.boardList);
} else {
this.boardList = await this.getBoardSearchList(param);
console.log(this.boardList);
}
} catch (error) {
console.log(error.response);
}
},
},
created() {
this.getBoardListMethod();
},
components: {
Pagination,
},
};
</script>
<style scoped src="../../assets/css/board/boardList.css"></style>
- 검색어를 입력하고 검색 버튼을 누르거나 엔터키를 누르면 getBoardListMethod를 실행시킨다.
- 검색을 한 경우에는 기본적으로 첫 번째 페이지를 보여줘야 하기 때문에 현재 페이지 번호를 1번으로 고정해서 param으로 사용하였고, 검색어가 없을 경우에는 전체 결과를 반환하는 getBoardList를, 검색어가 있을 경우에는 검색어로 결과를 찾는 getBoardSearchList 메소드를 실행해서 결과를 boardList 데이터에 넣었다.
- 전체 결과이든, 검색 결과이든 페이지를 전환하면 Pagination에서 발생한 이벤트와 payload로 전달된 인덱스를 이용해서 해당 페이지에 있는 게시글 리스트를 결과로 반환한다.
- 검색창 상단에는 정상적으로 검색이 실행되었는지 확인을 하기 위해 검색 결과 수를 표시하고, 검색 결과의 개수가 0일 경우 검색 결과가 없다는 메시지를 화면에 보여주도록 구현하였다.
0430
게시글 세부 정보 기능
게시글을 클릭하였을 때, 해당 게시물의 세부 정보를 불러와서 보여주고자 했다.
먼저 클릭했을 때 라우팅을 설정해야 하는데 이번에는 다른 페이지들과는 다르게 게시물 번호를 url에 포함하여 라우팅하고 싶었다.
이를 위해서 Dynamic Route Matching 기술을 사용했다.
Dynamic Route Matching
- 라우터에 path를 정의할 때 동적 세그먼트는 콜론 :으로 표시된다.
- 경로가 일치하면, 동적 세그먼트의 값은 this.$route.params로 모든 컴포넌트에서 사용 가능해진다.
// /router/index.js
const routes = [
{
path: "/post/:boardNo",
name: "detail",
component: () => import("@/views/PostDetail.vue"),
meta: { auth: true },
},
];
<!-- PostContent.vue -->
<script>
export default {
data() {
return {
boardNo: this.$route.params.boardNo,
};
},
};
</script>
- 예를 들어, /post/123 url로 접근하게 되면 해당 페이지의 데이터로 boardNo는 123 값이 저장되어 있다.
0501
게시글 상세 내용 확인 기능을 작성하고, 그에 맞게 화면을 디자인 했다.
<template>
<div class="content_mainBox">
<div class="info_area">
<span class="title"></span>
<div class="sub_info">
<span class="member_name">by </span>
<span class="date"></span>
</div>
</div>
<hr />
<div class="content_mainCont">
<div class="content">
</div>
<div class="like">
<button>좋아요</button>
좋아요
</div>
</div>
<hr />
<div class="content_reply">
<div class="content_replyBefore">
<table class="content_mainTable02">
<colgroup>
<col style="width: 10%;" />
<col />
<col style="width: 10%;" />
</colgroup>
<tr>
<td><!--이미지--></td>
<td><input type="text" class="reply" /></td>
<td><button class="btn_basic">댓글 입력</button></td>
</tr>
</table>
</div>
<div class="content_replyAfter">
<table class="content_mainTable02">
<colgroup>
<col style="width: 10%;" />
<col />
<col style="width: 10%;" />
</colgroup>
<tr>
<td><!--이미지--></td>
<td><!--입력된 내용--></td>
<td><button class="btn_basic">삭제</button></td>
</tr>
</table>
</div>
</div>
</div>
</template>
<script>
import { mapActions } from "vuex";
export default {
data() {
return {
boardNo: this.$route.params.boardNo,
boardInfo: {},
};
},
methods: {
...mapActions(["getBoardDetail"]),
async getBoardDetailMethod() {
try {
this.boardInfo = await this.getBoardDetail(this.boardNo);
console.log(this.boardInfo);
} catch (error) {
console.log(error.response);
}
},
},
created() {
this.getBoardDetailMethod();
},
};
</script>
<style scoped src="../../assets/css/post/content.css"></style>
- 동적 라우팅을 통해 받은 파라미터를 api를 호출할 때 사용할 변수로 사용했다.
- 결과 값으로 받아온 boardInfo의 속성들을 화면을 나타내는 데 적절하게 사용해서 구현하였다.
0502
좋아요 기능
각 게시물에 한 회원당 한 번의 좋아요를 클릭할 수 있다.
좋아요는 처음에 빈 하트로 존재하고, 클릭시 하트가 채워지며 좋아요 수가 증가한다.
이미 좋아한 글에 다시 들어올 때는 좋아요 하트가 채워진 채로 보여지게 된다.
<!-- template 영역 - 좋아요 부분만 제외하고는 생략 -->
<template>
<div class="like">
<i
class="far fa-heart heart"
v-if="boardInfo.likeYn == 'N'"
@click="clickLikeMethod"
>
</i>
<i
class="fas fa-heart heart"
v-if="boardInfo.likeYn == 'Y'"
@click="clickLikeMethod"
>
</i>
<span class="heart_cnt">좋아요 </span>
</div>
</template>
<script>
import { mapActions } from "vuex";
export default {
data() {
return {
boardNo: this.$route.params.boardNo,
boardInfo: {},
};
},
methods: {
...mapActions(["getBoardDetail", "clickLike"]),
async getBoardDetailMethod() {
try {
this.boardInfo = await this.getBoardDetail(this.boardNo);
console.log(this.boardInfo);
} catch (error) {
console.log(error.response);
}
},
async clickLikeMethod() {
try {
const boardNo = {
boardNo: this.boardNo,
};
await this.clickLike(boardNo);
await this.getBoardDetailMethod();
} catch (error) {
console.log(error.response);
}
},
},
created() {
this.getBoardDetailMethod();
},
};
</script>
- 좋아요 숫자를 업데이트 한 후, 업데이트 결과를 화면에 다시 그려주기 위해서 게시글 세부 정보 api를 재호출한다.