본문 바로가기
Programming/Vue

[실습] 뷰(Vue)로 회원/사용자 관리 화면 구현하기 - Backend(REST API) 연결하기

by 돌방로그 2023. 2. 20.


오늘의 실습 목표는 "회원 관리 기능 만들어보기!" 입니다.
본 게시글에서 다루는 사항은 회원 관리 화면에서 Backend로 연동하기 위해 REST API 연결하는 과정입니다.

 


회원/사용자 화면 구현하기 - 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 연결 함수

 

 


댓글