upload
This commit is contained in:
parent
f0645957fe
commit
aedf8f5824
@ -6,7 +6,7 @@ import { RouterLink, RouterView } from 'vue-router';
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/article">article</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
31
src/api/article.js
Normal file
31
src/api/article.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { localAxios } from '@/utils/http-commons';
|
||||
|
||||
const local = localAxios();
|
||||
|
||||
function getArticles(success, fail) {
|
||||
//list
|
||||
local.get('/article/list').then(success).catch(fail);
|
||||
}
|
||||
|
||||
function searchArticle(keyword, success, fail) {
|
||||
local.get(`/article/search?keyword=${keyword}`).then(success).catch(fail);
|
||||
}
|
||||
|
||||
function getArticle(id, success, fail) {
|
||||
//detail
|
||||
local.get(`/article/${id}`).then(success).catch(fail);
|
||||
}
|
||||
|
||||
function updateArticle(article, id, success, fail) {
|
||||
local.put(`/article/${id}`, JSON.stringify(article)).then(success).catch(fail);
|
||||
}
|
||||
|
||||
function deleteArticle(id, success, fail) {
|
||||
local.delete(`/article/${id}`).then(success).catch(fail);
|
||||
}
|
||||
|
||||
function addArticle(article, success, fail) {
|
||||
local.post('/article', JSON.stringify(article)).then(success).catch(fail);
|
||||
}
|
||||
|
||||
export { getArticles, searchArticle, getArticle, updateArticle, deleteArticle, addArticle };
|
139
src/components/article/ArticleDetail.vue
Normal file
139
src/components/article/ArticleDetail.vue
Normal file
@ -0,0 +1,139 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getArticle, updateArticle, deleteArticle } from '@/api/article';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const id = ref(route.query.id);
|
||||
|
||||
/*
|
||||
DOM이 mounted된 상태에서 아직 서버에서 데이터가 안 온 경우
|
||||
Book의 데이터를 화면에 표시 할 수 없어서 error 발생하므로 초기 설정을 한다.
|
||||
*/
|
||||
const article = ref({
|
||||
id: '',
|
||||
title: '',
|
||||
text: '',
|
||||
name: '',
|
||||
authorId: '',
|
||||
date: '',
|
||||
});
|
||||
|
||||
//setup(init)단계이므로 detailBook() 호출해서 서버에서 데이터 전송 받기
|
||||
getArticle(
|
||||
id.value,
|
||||
({ data }) => {
|
||||
article.value = data;
|
||||
},
|
||||
(error) => {
|
||||
alert(error);
|
||||
}
|
||||
);
|
||||
|
||||
//초기에는 Detail화면 표시 하기 위해 readonly='readonly' 로 표시
|
||||
//수정일 경우 readonly='' 표시하기 위해 isReadonly=false로
|
||||
const isReadonly = ref(true);
|
||||
|
||||
function removeHandler() {
|
||||
// console.log('BookDetail.remove.......................')
|
||||
// books.value = books.value.filter((item) => isbn.value != item.isbn)
|
||||
// console.log('BookDetail.remove.......................books:', books.value)
|
||||
deleteArticle(
|
||||
id.value,
|
||||
({ data }) => {
|
||||
console.log('deleteArticle................result:', data);
|
||||
alert('삭제 성공');
|
||||
moveHandler();
|
||||
},
|
||||
(error) => {
|
||||
alert('삭제 실패');
|
||||
}
|
||||
);
|
||||
// moveHandler()
|
||||
}
|
||||
|
||||
function moveHandler() {
|
||||
router.push({
|
||||
name: 'article-list',
|
||||
});
|
||||
}
|
||||
|
||||
function updateHandler() {
|
||||
if (!isReadonly.value) {
|
||||
//isReadonly가 false인 상황은 update 상황
|
||||
updateArticle(
|
||||
article.value,
|
||||
id,
|
||||
({ data }) => {
|
||||
console.log('updateArticl................result:', data);
|
||||
alert('수정 성공');
|
||||
},
|
||||
(error) => {
|
||||
alert('수정 실패');
|
||||
}
|
||||
);
|
||||
}
|
||||
isReadonly.value = !isReadonly.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>게시판 번호</th>
|
||||
<td><input type="text" v-model.lazy="book.isbn" readonly="readonly" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>제목</th>
|
||||
<td><input type="text" v-model.lazy="book.title" :readonly="isReadonly" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>내용</th>
|
||||
<td><input type="text" v-model.lazy="book.author" :readonly="isReadonly" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>작성자</th>
|
||||
<td><input type="text" v-model.lazy="book.price" :readonly="isReadonly" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>아이디</th>
|
||||
<td><input type="text" v-model.lazy="book.price" :readonly="isReadonly" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>날짜</th>
|
||||
<td><input type="text" v-model.lazy="book.price" :readonly="isReadonly" /></td>
|
||||
</tr>
|
||||
<!-- <tr>
|
||||
<th colspan="2">책 정보</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<textarea
|
||||
cols="45"
|
||||
rows="10"
|
||||
v-model.lazy="book.describ"
|
||||
:readonly="isReadonly"
|
||||
></textarea>
|
||||
</td>
|
||||
</tr> -->
|
||||
<tr>
|
||||
<td colspan="2" class="text-center">
|
||||
<button class="btn btn-outline-primary m-1" @click="updateHandler">수정</button>
|
||||
<button class="btn btn-outline-primary m-1" @click="moveHandler">목록</button>
|
||||
<button class="btn btn-outline-primary m-1" @click="removeHandler">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
input:read-only {
|
||||
background-color: lightgray;
|
||||
}
|
||||
</style>
|
126
src/components/article/ArticleList.vue
Normal file
126
src/components/article/ArticleList.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import { getArticles } from '@/api/article';
|
||||
import { ref, reactive } from 'vue';
|
||||
import ArticleListItem from './item/ArticleListItem.vue';
|
||||
import VSelect from '@/components/common/VSelect.vue';
|
||||
import PageNavigation from '@/components/common/PageNavigation.vue';
|
||||
|
||||
//목록을 표시할 books를 반응형으로 선언
|
||||
const articles = ref([]);
|
||||
|
||||
// axios를 통해 서버에 보낼 조회 조건을 반응형 변수로 선언.
|
||||
// 이때 axios의 params로 데이터를 보낼때는 json으로 변형되서 전달되므로 ref()보다는
|
||||
// reactive()로 반응형 변수를 만들어야 오류가 없다.
|
||||
const params = reactive({
|
||||
pageNo: 1,
|
||||
key: 'all',
|
||||
keyword: '',
|
||||
});
|
||||
|
||||
const currentPage = ref(1);
|
||||
const totalpage = ref(1);
|
||||
|
||||
//VSelect 컴포넌트의 props
|
||||
const selectOptions = ref([
|
||||
{ text: '---선택하세요---', value: 'all' },
|
||||
{ text: '제목+내용', value: 'title' },
|
||||
// { text: '저자', value: 'author' },
|
||||
]);
|
||||
|
||||
function searchList() {
|
||||
console.log('searchList....params:', params);
|
||||
searchArticle(
|
||||
params,
|
||||
({ data }) => {
|
||||
console.log(data);
|
||||
articles.value = data.articles;
|
||||
// currentPage.value = data.page.pageNo;
|
||||
// totalpage.value = data.page.total;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
// searchList(); //setup(init단계)에서 searchList()를 호출해서 서버에서 데이터 전달 받기
|
||||
|
||||
getArticles(
|
||||
({ data }) => {
|
||||
console.log(data);
|
||||
articles.value = data;
|
||||
// currentPage.value = data.page.pageNo;
|
||||
// totalpage.value = data.page.total;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
|
||||
const changeKey = (val) => {
|
||||
console.log('search.....................changeKey:', val);
|
||||
params.key = val;
|
||||
console.log('search.....................key:', params.key);
|
||||
};
|
||||
|
||||
//PageNavigation에서 오는 emit 이벤트를 처리 하기 위한 함수
|
||||
function pageChange(value) {
|
||||
params.pageNo = value;
|
||||
searchList();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<h3 class="text-center">등록된 도서 목록</h3>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<router-link :to="{ name: 'article-regist' }">
|
||||
<button class="btn btn-outline-primary">등록</button>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">검색조건</span>
|
||||
<v-select :selectOptions="selectOptions" @on-key-select="changeKey" />
|
||||
<input
|
||||
type="text"
|
||||
class="input-control"
|
||||
placeholder="검색어를 입력하세요"
|
||||
v-model="params.word"
|
||||
@keyup.enter="searchList"
|
||||
/>
|
||||
<button class="btn btn-dark" @click="searchList">검색</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="articles.length > 0">
|
||||
<table class="table table-boardered table-hover">
|
||||
<thead>
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="40%" />
|
||||
<col width="15%" />
|
||||
<col width="20%" />
|
||||
</colgroup>
|
||||
<tr class="text-center">
|
||||
<th>게시글 번호</th>
|
||||
<th>제목</th>
|
||||
<th>작성자</th>
|
||||
<th>아이디</th>
|
||||
<th>날짜</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ArticleListItem
|
||||
v-for="article in articles"
|
||||
:key="article.id"
|
||||
:article="article"
|
||||
></ArticleListItem>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3>등록된 책 정보가 없습니다.</h3>
|
||||
</div>
|
||||
<PageNavigation :currentPage="currentPage" :totalPage="totalpage" @page-change="pageChange" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
129
src/components/article/ArticleRegist.vue
Normal file
129
src/components/article/ArticleRegist.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { addArticle } from '@/api/article';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
//v-model과 연결한 반응형 변수 선언
|
||||
const title = ref('');
|
||||
const text = ref('');
|
||||
const name = ref('');
|
||||
const authorId = ref('');
|
||||
|
||||
/*
|
||||
Dom과 연결할 반응형 변수 선언, setup에서는 DOM tree가 구성되지 않았기 때문에
|
||||
mounted 이후에 접근이 가능하다.
|
||||
*/
|
||||
const titleDiv = ref(null);
|
||||
const textDiv = ref(null);
|
||||
const nameDiv = ref(null);
|
||||
const authorIdDiv = ref(null);
|
||||
|
||||
// const books = inject('books')
|
||||
// console.log('BookRegist....books:', books.value)
|
||||
|
||||
function moveHandler() {
|
||||
router.push({ name: 'article-list' });
|
||||
}
|
||||
|
||||
function createHandler() {
|
||||
//사용자 입력 값을 체크하기
|
||||
let err = false;
|
||||
let msg = '';
|
||||
|
||||
// if (!isbn.value) {
|
||||
// msg = '책 일련 번호를 입력해 주세요'
|
||||
// err = false;
|
||||
// isbnDiv.value.focus()
|
||||
// } else if (err && !title.value) { }
|
||||
!title.value && ((msg = '제목을 입력해 주세요'), (err = true), titleDiv.value.focus());
|
||||
!err && !text.value && ((msg = '내용을 입력해 주세요'), (err = true), textDiv.value.focus());
|
||||
!err && !name.value && ((msg = '이름을 입력해 주세요'), (err = true), nameDiv.value.focus());
|
||||
!err &&
|
||||
!authorId.value &&
|
||||
((msg = '아이디를 입력해 주세요'), (err = true), authorIdDiv.value.focus());
|
||||
|
||||
const article = ref({
|
||||
title: '',
|
||||
text: '',
|
||||
name: '',
|
||||
authorId: '',
|
||||
});
|
||||
|
||||
if (err) {
|
||||
alert(msg);
|
||||
} else {
|
||||
article.value.title = title.value;
|
||||
article.value.text = text.value;
|
||||
article.value.name = name.value;
|
||||
article.value.authorId = authorId.value;
|
||||
}
|
||||
// console.log('bookregist.......', books.value)
|
||||
|
||||
// title.value = ''
|
||||
// isbn.value = ''
|
||||
// author.value = ''
|
||||
// price.value = ''
|
||||
// describ.value = ''
|
||||
// alert('등록')
|
||||
// moveHandler()
|
||||
// }
|
||||
addArticle(
|
||||
article.value,
|
||||
({ data }) => {
|
||||
console.log('update................result:', data);
|
||||
alert('등록 성공');
|
||||
},
|
||||
(error) => {
|
||||
alert('수정 실패');
|
||||
}
|
||||
);
|
||||
|
||||
moveHandler();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="me-4">
|
||||
<h1 class="underline text-center">도서 등록</h1>
|
||||
<table class="table table-boardered">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>제목</td>
|
||||
<td><input type="text" ref="titleDiv" v-model="title" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>내용</td>
|
||||
<td><input type="text" ref="textDiv" v-model="text" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>이름</td>
|
||||
<td><input type="text" ref="nameDiv" v-model="name" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>아이디</td>
|
||||
<td><input type="text" ref="authorIdDiv" v-model="authorId" /></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- <tr>
|
||||
<td colspan="2">책 정보</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center">
|
||||
<textarea v-model="describ" cols="45" rows="10" />
|
||||
</td>
|
||||
</tr> -->
|
||||
<tr>
|
||||
<td colspan="2" class="text-center">
|
||||
<button class="btn btn-primary m-1" @click="createHandler">등록</button>
|
||||
<button class="btn btn-primary m-1" @click="moveHandler">목록</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
16
src/components/article/item/ArticleListItem.vue
Normal file
16
src/components/article/item/ArticleListItem.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
defineProps({ article: Object });
|
||||
</script>
|
||||
<template>
|
||||
<tr class="text-center">
|
||||
<th>{{ article.isbn }}</th>
|
||||
<td>
|
||||
<router-link :to="{ name: 'article-detail', query: { id: article.id } }">
|
||||
{{ article.title }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td>{{ article.author }}</td>
|
||||
<td>{{ article.price }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
<style scoped></style>
|
65
src/components/common/PageNavigation.vue
Normal file
65
src/components/common/PageNavigation.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({ currentPage: Number, totalPage: Number })
|
||||
const emit = defineEmits(['pageChange'])
|
||||
|
||||
// const navigationSize = parseInt(import.meta.env.VITE_ARTICLE_NAVIGATION_SIZE)
|
||||
const navigationSize = 10
|
||||
|
||||
const startPage = computed(() => {
|
||||
return parseInt((props.currentPage - 1) / navigationSize) * navigationSize + 1
|
||||
})
|
||||
|
||||
const endPage = computed(() => {
|
||||
let lastPage =
|
||||
parseInt((props.currentPage - 1) / navigationSize) * navigationSize + navigationSize
|
||||
return props.totalPage < lastPage ? props.totalPage : lastPage
|
||||
})
|
||||
|
||||
const endRange = computed(() => {
|
||||
return parseInt((props.totalPage - 1) / navigationSize) * navigationSize < props.currentPage
|
||||
})
|
||||
|
||||
function range(start, end) {
|
||||
const list = []
|
||||
for (let i = start; i <= end; i++) list.push(i)
|
||||
return list
|
||||
// return Array(end - start + 1)
|
||||
// .fill()
|
||||
// .map((val, i) => start + i);
|
||||
}
|
||||
|
||||
function onPageChange(pg) {
|
||||
console.log(pg + '로 이동!!!')
|
||||
emit('pageChange', pg)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item">
|
||||
<a class="page-link" @click="onPageChange(1)">최신</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" @click="onPageChange(startPage == 1 ? 1 : startPage - 1)">이전</a>
|
||||
</li>
|
||||
<template v-for="pg in range(startPage, endPage)" :key="pg">
|
||||
<li :class="currentPage === pg ? 'page-item active' : 'page-item'">
|
||||
<a class="page-link" @click="onPageChange(pg)">{{ pg }}</a>
|
||||
</li>
|
||||
</template>
|
||||
<li class="page-item">
|
||||
<a class="page-link" @click="onPageChange(endRange ? totalPage : endPage + 1)">다음</a>
|
||||
</li>
|
||||
<li class="page-item"><a class="page-link" @click="onPageChange(totalPage)">마지막</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
20
src/components/common/VSelect.vue
Normal file
20
src/components/common/VSelect.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import { ref} from 'vue'
|
||||
defineProps({ selectOptions: Array })
|
||||
const emit = defineEmits(["onKeySelect"])
|
||||
const key = ref('all')
|
||||
const onSelect = () => {
|
||||
console.log('vselect................... key:', key.value)
|
||||
emit('onKeySelect', key.value)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<select class="form-select" v-model="key" @change="onSelect">
|
||||
<option v-for="(k, index) in selectOptions" :key="index" :value="k.value">
|
||||
{{k.text}}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -9,6 +9,30 @@ const router = createRouter({
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: '/article',
|
||||
name: 'article',
|
||||
//index.html이 화면에 표시될때 미리 다운 받지 않고 필요시 동적으로 페이지를 다운 받는다.
|
||||
component: () => import('@/views/ArticleView.vue'),
|
||||
redirect: { name: 'article-list' },
|
||||
children: [
|
||||
{
|
||||
path: 'list', //children은 /를 자동으로 붙여 주므로 빼야 한다.
|
||||
name: 'article-list',
|
||||
component: () => import('@/components/article/ArticleList.vue'),
|
||||
},
|
||||
{
|
||||
path: 'regist',
|
||||
name: 'article-regist',
|
||||
component: () => import('@/components/article/ArticleRegist.vue'),
|
||||
},
|
||||
{
|
||||
path: 'detail',
|
||||
name: 'article-detail',
|
||||
component: () => import('@/components/article/ArticleDetail.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
15
src/utils/http-commons.js
Normal file
15
src/utils/http-commons.js
Normal file
@ -0,0 +1,15 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// const { VITE_API_BASE_URL } ="http://localhost/";
|
||||
|
||||
function localAxios() {
|
||||
const instance = axios.create({
|
||||
baseURL: 'http://localhost',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
},
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
export { localAxios };
|
10
src/views/ArticleView.vue
Normal file
10
src/views/ArticleView.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<script setup>
|
||||
import { ref, provide, watch } from 'vue';
|
||||
</script>
|
||||
<template>
|
||||
<div class="text-center m-4">
|
||||
<div class="alert alert-info" role="alert">게시판</div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped></style>
|
Loading…
Reference in New Issue
Block a user