
오늘의 실습 목표는 "회원 관리 기능 만들어보기!" 입니다.
본 게시글에서 다루는 사항은 회원 관리 화면에서 Backend로 연동하기 위해 REST API 연결하는 과정입니다.
회원/사용자 화면 구현하기 - REST API 호출/연동하기
사전 준비
아래 사항에 대해서 사전 준비가 완료되지 않으신 분들은 아래 링크를 참조하여 사전 준비를 진행합니다.
- [실습] 뷰(Vue)로 회원/사용자 관리 화면 구현하기 - UI/Layout 구현하기
- [실습] 스프링부트(Spring Boot)로 회원 관리 기능 구현하기 - REST API 구현하기
결과 이미지
구현하기에 앞서 본 글을 따라하시면 아래와 같이 화면과 API Server가 연동된 결과를 확인하실 수 있습니다.
▶ 초기 상태
DB (MySQL)

화면

▶ REGISTER
DB (MySQL)

화면


▶ MODIFY
DB (MySQL)

화면


▶ DELETE
DB (MySQL)

화면


소스 코드
src\components\admin-web\users\AdminUsersMain.vue
전체 코드를 확인하실 분들은 아래 더보기를 클릭해주세요.
더보기
<template>
<v-container>
<div class="text-h5 font-weight-medium ma-2">Users (Admin Web)</div>
<v-divider></v-divider>
<v-row>
<v-col class="align-self-center" :cols="4">
<div>
<v-switch
inset
color="indigo darken-3"
v-model="readAll"
:label="`${readAll ? 'All' : 'Available'}`"
@change="changeReadStatus"
></v-switch>
</div>
</v-col>
<v-col class="align-self-center" :cols="8">
<div class="d-flex justify-end">
<v-dialog persistent max-width="1000px" v-model="dialogRegister">
<template v-slot:activator="{ on, attrs }">
<v-btn
text
outlined
class="mx-md-1 elevation-2"
v-bind="attrs"
v-on="on"
>REGISTER
</v-btn>
</template>
<v-card>
<Form
:mode="'REGISTER'"
v-on:finishProcess="finishProcess"
></Form>
</v-card>
</v-dialog>
<v-dialog persistent max-width="1000px" v-model="dialogModify">
<template v-slot:activator="{ on, attrs }">
<v-btn
text
outlined
class="mx-md-1 elevation-2"
v-bind="attrs"
v-on="on"
:disabled="!hasSelectedRow()"
>MODIFY
</v-btn>
</template>
<v-card>
<Form
:mode="'MODIFY'"
:employee="this.selectedRow[0]"
v-on:finishProcess="finishProcess"
></Form>
</v-card>
</v-dialog>
<v-dialog v-model="dialogDelete" persistent max-width="300">
<template v-slot:activator="{ on, attrs }">
<v-btn
text
outlined
class="mx-md-1 elevation-2"
:disabled="!checkDeleteStatus()"
v-bind="attrs"
v-on="on"
>DELETE
</v-btn>
</template>
<v-card>
<v-card-title class="text-h5">
Are you sure you want to delete the user?
</v-card-title>
<v-card-text
>Deleted notices cannot be restored, and are immediately
reflected on the user portal after deletion.</v-card-text
>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="dialogDelete = false">
Cancel
</v-btn>
<v-btn color="blue darken-1" text @click="deleteUser">
Confirm
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</v-col>
</v-row>
<div>
<v-card-title>
Admin Users
<v-spacer></v-spacer>
<v-text-field
single-line
hide-details
v-model="search"
append-icon="mdi-magnify"
label="Search"
></v-text-field>
</v-card-title>
<v-data-table
single-select
show-select
item-key="employeeNo"
show-expand
class="mt-md-2 elevation-5"
v-model="selectedRow"
:search="search"
:headers="headers"
:items="users"
:items-per-page="10"
>
<template v-slot:expanded-item="{ headers, item }">
<td class="pa-5" :colspan="headers.length">
{{ "- Division: " + item.division }} <br />{{
"- Team: " + item.team
}}
<br />{{ "- Name (Eng): " + item.nameEng }} <br />{{
"- Email: " + item.email
}}
<br />{{ "- Phone: " + item.phone }}
</td>
</template>
</v-data-table>
</div>
</v-container>
</template>
<script>
import Form from "./../../../components/admin-web/users/AdminUsersDetail.vue";
export default {
components: {
Form,
},
data() {
return {
today: new Date(Date.now() - new Date().getTimezoneOffset() * 60000)
.toISOString()
.substring(0, 10)
.replaceAll("-", ""),
readAll: false,
dialogRegister: false,
dialogModify: false,
dialogDelete: false,
selectedRow: [],
search: "",
headers: [
{
text: "Company",
value: "company",
},
{
text: "Employee No.",
value: "employeeNo",
},
{
text: "Name (Kor)",
value: "nameKor",
},
{
text: "Position",
value: "position",
},
],
users: [],
};
},
created: function () {
this.getAdminUsers();
},
methods: {
hasSelectedRow: function () {
return this.selectedRow.length > 0;
},
checkDeleteStatus: function () {
let isAvailable = false;
if (this.hasSelectedRow() === true) {
isAvailable = this.selectedRow[0].usageExpDate > this.today;
}
return isAvailable;
},
finishProcess: function () {
this.dialogRegister = false;
this.dialogModify = false;
window.location.reload();
},
changeReadStatus: function () {
this.getAdminUsers();
},
deleteUser: function () {
this.dialogDelete = false;
this.$axios
.put("/api/admin-web/users/delete/" + this.selectedRow[0].employeeNo, {
employeeNo: this.selectedRow[0].employeeNo,
// TODO: EmployeeNo는 로그인한 유저의 사번으로 변경
updateEmployeeNo: "000000",
usageExpDate: this.today,
})
.catch(function (error) {
console.log("[ERR/DEL]" + error);
});
},
getAdminUsers: function () {
let path = "/api/admin-web/users/";
path += this.readAll ? "all" : "available";
this.$axios
.get(path)
.then((response) => {
this.users = [];
for (let idx in response.data) {
let user = {
company: response.data[idx].employeeCompany,
employeeNo: response.data[idx].employeeNo,
nameKor: response.data[idx].employeeName,
position: response.data[idx].employeePosition,
division: response.data[idx].employeeDivision,
team: response.data[idx].employeeTeam,
email: response.data[idx].employeeEmail,
phone: response.data[idx].employeePhone,
nameEng: response.data[idx].employeeNameEng,
usageExpDate: response.data[idx].usageExpDate,
};
this.users.push(user);
}
})
.catch(function (error) {
console.log("[ERR/REG]" + error);
});
},
},
};
</script>
<style scoped></style>
변경 사항
<template>
<template>
<v-container>
...
<v-row>
...
<v-switch
...
@change="changeReadStatus"
></v-switch>
...
</v-row>
...
<v-dialog persistent max-width="1000px" v-model="dialogRegister">
...
<Form
:mode="'REGISTER'"
v-on:finishProcess="finishProcess"
></Form>
...
</v-dialog>
...
<v-dialog persistent max-width="1000px" v-model="dialogModify">
...
<Form
:mode="'MODIFY'"
v-on:finishProcess="finishProcess"
></Form>
...
</v-dialog>
...
</v-container>
</template>
- <v-switch>: @change로 토글 버튼의 변화를 감지하여 처리하기 위해 함수 정의
- <Form>: 다이얼로그에 선택한 버튼(모드/mode)이 무엇인지 props로 전달
<script>
<script>
...
created: function () {
this.getAdminUsers();
},
methods: {
changeReadStatus: function () {
this.getAdminUsers();
},
deleteUser: function () {
...
this.$axios
.put("/api/admin-web/users/delete/" + this.selectedRow[0].employeeNo, {
employeeNo: this.selectedRow[0].employeeNo,
// TODO: EmployeeNo는 로그인한 유저의 사번으로 변경
updateEmployeeNo: "000000",
usageExpDate: this.today,
})
.catch(function (error) {
console.log("[ERR/DEL]" + error);
});
},
getAdminUsers: function () {
let path = "/api/admin-web/users/";
path += this.readAll ? "all" : "available";
this.$axios
.get(path)
.then((response) => {
this.users = [];
for (let idx in response.data) {
let user = {
company: response.data[idx].employeeCompany,
employeeNo: response.data[idx].employeeNo,
nameKor: response.data[idx].employeeName,
position: response.data[idx].employeePosition,
division: response.data[idx].employeeDivision,
team: response.data[idx].employeeTeam,
email: response.data[idx].employeeEmail,
phone: response.data[idx].employeePhone,
nameEng: response.data[idx].employeeNameEng,
usageExpDate: response.data[idx].usageExpDate,
};
this.users.push(user);
}
})
.catch(function (error) {
console.log("[ERR/REG]" + error);
});
},
},
};
</script>
- created: 초기 생성시 <v-switch>가 설정된 값에 따라 데이터 조회 API 호출 함수 연결
- methods
- chageReadStatus(): <v-switch>로 정의된 토글 버튼이 변경될 때 호출되는 이벤트 처리 함수
- deleteUser(): 삭제 다이얼로그에서 OK 버튼이 클릭됬을 때 이벤트 처리 함수
- getAdminUsers(): 데이터 조회 REST API 연결 프로세스가 구현된 함수로 <v-switch>로 선택된 값에 따라 다른 READ API 호출
src\components\admin-web\users\AdminUsersDetail.vue
전체 코드를 확인하실 분들은 아래 더보기를 클릭해주세요.
더보기
<template>
<v-container>
<div class="text-h5 font-weight-medium ma-2">
User Information (Admin Web)
</div>
<v-divider class="mb-5"></v-divider>
<div class="ma-md-2">
<v-row>
<v-col>
<v-combobox
outlined
dense
hide-selected
required
clearable
:disabled="isModifyMode()"
:items="companies"
:rules="[rules.required]"
v-model="company"
label="회사"
@change="changeCompany"
></v-combobox>
</v-col>
<v-col>
<v-text-field
text
outlined
dense
clearable
:disabled="isModifyMode()"
:rules="[rules.required, rules.employeeNo]"
v-model="employeeNo"
label="직원번호"
placeholder="000000"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
text
outlined
dense
clearable
:rules="[rules.required]"
v-model="nameKor"
label="국문이름"
placeholder="홍길동"
></v-text-field>
</v-col>
<v-col>
<v-text-field
text
outlined
dense
clearable
v-model="nameEng"
label="영문이름"
placeholder="Gildong Hong"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
text
outlined
dense
clearable
:rules="[rules.required, rules.phone]"
v-model="phone"
label="휴대폰 번호"
placeholder="010-####-####"
></v-text-field>
</v-col>
<v-col>
<v-text-field
text
outlined
dense
clearable
type="email"
:rules="[rules.required, rules.email]"
v-model="email"
label="이메일 주소"
placeholder="gildong-hong@gmail.com"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
text
outlined
dense
clearable
:append-icon="showPw ? 'mdi-eye' : 'mdi-eye-off'"
:type="showPw ? 'text' : 'password'"
:rules="[rules.required, rules.password]"
v-model="password"
label="비밀번호"
@click:append="showPw = !showPw"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
text
outlined
dense
clearable
v-model="division"
label="부서"
></v-text-field>
</v-col>
<v-col>
<v-text-field
text
outlined
dense
clearable
v-model="team"
label="팀"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
text
outlined
dense
clearable
:rules="[rules.required]"
v-model="position"
label="직책"
></v-text-field>
</v-col>
<v-col>
<v-text-field
text
outlined
dense
clearable
type="date"
:rules="[rules.required]"
:min="minUsageExpiryDate"
v-model="usageExpiryDate"
label="사용기한일자"
></v-text-field>
</v-col>
</v-row>
</div>
<div class="d-flex justify-end">
<v-btn
text
outlined
dense
class="mx-md-1 elevation-2"
@click="clickCancel"
>Cancel
</v-btn>
<v-btn text outlined dense class="mx-md-1 elevation-2" @click="clickOk"
>OK
</v-btn>
</div>
</v-container>
</template>
<script>
const MODE_REGISTER = "REGISTER";
const MODE_MODIFY = "MODIFY";
export default {
props: ["employee", "mode"],
data() {
return {
rules: {
required: (value) => !!value || "Required.",
employeeNo: (value) =>
value.length == 6 || "직원번호는 6자리로 구성되어야 합니다.",
phone: (value) => {
const pattern = /^(\d{3})-(\d{4})-(\d{4})$/;
return pattern.test(value) || "휴대폰 번호 형식에 맞지 않습니다.";
},
email: (value) => {
const pattern =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return pattern.test(value) || "이메일 주소 형식에 맞지 않습니다.";
},
password: (value) => {
const pattern = /^(?=.*\d)(?=.*[a-z]).{8,}$/;
return (
pattern.test(value) ||
"영어 소문자, 숫자를 포함하여 8자리 이상으로 구성되어야 합니다."
);
},
},
minUsageExpiryDate: new Date(
Date.now() - new Date().getTimezoneOffset() * 60000
)
.toISOString()
.substring(0, 10),
companies: ["봄 회사", "여름 컴퍼니", "(주) 가을", "겨울 패밀리"],
showPw: false,
// V-MODEL
company: [],
employeeNo: "",
nameKor: "",
nameEng: "",
phone: "",
email: "",
password: "",
division: "",
team: "",
position: "",
usageExpiryDate: "9999-12-31",
};
},
mounted: function () {
this.$nextTick(function () {
if (this.mode === MODE_MODIFY) {
this.setData();
}
});
},
watch: {
employee: {
handler() {
this.setData();
},
},
},
methods: {
setData: function () {
this.company = this.employee.company;
this.employeeNo = this.employee.employeeNo;
this.nameKor = this.employee.nameKor;
this.nameEng = this.employee.nameEng;
this.phone = this.employee.phone;
this.email = this.employee.email;
this.division = this.employee.division;
this.team = this.employee.team;
this.position = this.employee.position;
},
changeCompany: function () {
switch (this.company) {
case "봄 회사": // 100000 ~ 199999
this.employeeNo = "1";
break;
case "여름 컴퍼니": // 300000 ~ 399999
this.employeeNo = "3";
break;
case "(주) 가을": // 500000 ~ 599999
this.employeeNo = "5";
break;
case "겨울 패밀리": // 700000 ~ 799999
this.employeeNo = "7";
break;
default:
// error
break;
}
},
isModifyMode: function () {
return this.mode === MODE_MODIFY;
},
clickOk: function () {
let path = "/api/admin-web/users";
switch (this.mode) {
case MODE_REGISTER:
console.log("REGISTER PROCESS");
this.$axios
.post(path, {
employeeNo: this.employeeNo,
registerEmployeeNo: "000000",
updateEmployeeNo: "000000",
employeeName: this.nameKor,
employeeNameEng: this.nameEng,
employeePhone: this.phone,
employeeEmail: this.email,
employeePw: this.password,
employeeCompany: this.company,
employeeDivision: this.division,
employeeTeam: this.team,
employeePosition: this.position,
usageExpDate: this.usageExpiryDate.replaceAll("-", ""),
})
.catch(function (error) {
console.log("[ERR/REG]" + error);
});
break;
case MODE_MODIFY:
console.log("MODIFY PROCESS");
path += "/update/";
this.$axios
.put(path + this.employeeNo, {
employeeNo: this.employeeNo,
updateEmployeeNo: "000000",
employee_name: this.nameKor,
employee_name_eng: this.nameEng,
employee_phone: this.phone,
employee_email: this.email,
employeePw: this.password,
employee_company: this.company,
employee_division: this.division,
employee_team: this.team,
employee_position: this.position,
usage_exp_date: this.usageExpiryDate,
})
.catch(function (error) {
console.log("[ERR/MOD]" + error);
});
break;
default:
// error
console.log("[ERR/clickOk] Mode:" + this.mode);
break;
}
this.$emit("finishProcess");
},
clickCancel: function () {
this.$emit("finishProcess");
},
},
};
</script>
<style scoped></style>
변경 사항
<script>
clickOk: function () {
let path = "/api/admin-web/users";
switch (this.mode) {
case MODE_REGISTER:
console.log("REGISTER PROCESS");
this.$axios
.post(path, {
employeeNo: this.employeeNo,
registerEmployeeNo: "000000",
updateEmployeeNo: "000000",
employeeName: this.nameKor,
employeeNameEng: this.nameEng,
employeePhone: this.phone,
employeeEmail: this.email,
employeePw: this.password,
employeeCompany: this.company,
employeeDivision: this.division,
employeeTeam: this.team,
employeePosition: this.position,
usageExpDate: this.usageExpiryDate.replaceAll("-", ""),
})
.catch(function (error) {
console.log("[ERR/REG]" + error);
});
break;
case MODE_MODIFY:
console.log("MODIFY PROCESS");
path += "/update/";
this.$axios
.put(path + this.employeeNo, {
employeeNo: this.employeeNo,
updateEmployeeNo: "000000",
employee_name: this.nameKor,
employee_name_eng: this.nameEng,
employee_phone: this.phone,
employee_email: this.email,
employeePw: this.password,
employee_company: this.company,
employee_division: this.division,
employee_team: this.team,
employee_position: this.position,
usage_exp_date: this.usageExpiryDate,
})
.catch(function (error) {
console.log("[ERR/MOD]" + error);
});
break;
default:
// error
console.log("[ERR/clickOk] Mode:" + this.mode);
break;
}
this.$emit("finishProcess");
},
...
</script>
- clickOk(): 사용자 등록, 사용자 정보 수정을 위한 REST API 연결 함수

'Programming > Vue' 카테고리의 다른 글
[실습] 뷰(Vue)로 설정 화면 구현하기 - UI/Layout 구현하기 (0) | 2023.03.02 |
---|---|
[실습] 뷰(Vue)로 설정 화면 구현하기 - 기획/설계하기 (0) | 2023.03.01 |
[실습] 뷰(Vue)로 회원/사용자 관리 화면 구현하기 - UI/Layout 구현하기 (0) | 2023.02.17 |
[실습] 뷰(Vue)로 회원/사용자 관리 화면 구현하기 - 기획/설계하기 (0) | 2023.02.16 |
[실습] 뷰(Vue)로 공지사항/게시판 구현하기 - Axios 이용하여 REST API 호출/연동하기 (0) | 2023.02.10 |
댓글