본문 바로가기
Programming/Vue

[실습] 뷰(Vue)로 설정 화면 구현하기 - Backend(REST API) 연결하기

by 돌방로그 2023. 3. 3.


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


설정 화면 구현하기 - REST API 호출/연동하기

사전 준비

아래 사항에 대해서 사전 준비가 완료되지 않으신 분들은 아래 링크를 참조하여 사전 준비를 진행합니다.

 


결과 이미지

구현하기에 앞서 본 글을 따라하시면 아래와 같이 화면과 API Server가 연동된 결과를 확인하실 수 있습니다.

 

▶ 초기 상태

DB (MySQL)

[TABLE] admin_user_information

 

[TABLE] admin_user_password_history

 

화면

 

▶ 본인 정보 변경

DB (MySQL)

[TABLE] admin_user_information

  • 휴대폰 번호가 010-1234-1234에서 010-1234-5678로 변경된 것을 확인

 

화면

 

▶ 비밀번호 수정

DB (MySQL)

[TABLE] admin_user_information

  • employee_pw와 employee_pw_exp_date의 값이 변경됨을 확인

 

[TABLE] admin_user_password_history

  • 변경된 비밀번호 정보가 신규 추가(no=2)됨을 확인

 

화면

 


소스 코드

src\components\admin-web\users\AdminUsersDetail.vue

전체 코드를 확인하실 분들은 아래 더보기를 클릭해주세요.

더보기
<template>
  <v-container>
    <div class="text-h5 font-weight-medium ma-2">
      {{ mode }}
    </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() || isSettingMode()"
            :items="companies"
            :rules="[rules.required]"
            v-model="employeeCompany"
            label="회사"
            @change="changeCompany"
          ></v-combobox>
        </v-col>
        <v-col>
          <v-text-field
            text
            outlined
            dense
            clearable
            :disabled="isModifyMode() || isSettingMode()"
            :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
            :disabled="isSettingMode()"
            :rules="[rules.required]"
            v-model="employeeName"
            label="국문이름"
            placeholder="홍길동"
          ></v-text-field>
        </v-col>
        <v-col>
          <v-text-field
            text
            outlined
            dense
            clearable
            :disabled="isSettingMode()"
            v-model="employeeNameEng"
            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="employeePhone"
            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="employeeEmail"
            label="이메일 주소"
            placeholder="gildong-hong@gmail.com"
          ></v-text-field>
        </v-col>
      </v-row>

      <v-row>
        <v-col v-show="!isSettingMode()">
          <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="employeePw"
            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="employeeDivision"
            label="부서"
          ></v-text-field>
        </v-col>
        <v-col>
          <v-text-field
            text
            outlined
            dense
            clearable
            v-model="employeeTeam"
            label="팀"
          ></v-text-field>
        </v-col>
      </v-row>

      <v-row>
        <v-col>
          <v-text-field
            text
            outlined
            dense
            clearable
            :rules="[rules.required]"
            v-model="employeePosition"
            label="직책"
          ></v-text-field>
        </v-col>
        <v-col>
          <v-text-field
            text
            outlined
            dense
            clearable
            v-show="!isSettingMode()"
            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";
const MODE_SETTING = "SETTING";

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
      employeeCompany: [],
      employeeNo: "",
      employeeName: "",
      employeeNameEng: "",
      employeePhone: "",
      employeeEmail: "",
      employeePw: "",
      employeeDivision: "",
      employeeTeam: "",
      employeePosition: "",
      usageExpiryDate: "9999-12-31",
    };
  },
  mounted: function () {
    this.$nextTick(function () {
      console.log(this.mode.toUpperCase());
      switch (this.mode.toUpperCase()) {
        case MODE_MODIFY:
        case MODE_SETTING:
          this.setData();
          break;
        default:
          break;
      }
    });
  },
  watch: {
    employee: {
      handler() {
        this.setData();
      },
    },
  },
  methods: {
    setData: function () {
      this.employeeCompany = this.employee.employeeCompany;
      this.employeeNo = this.employee.employeeNo;
      this.employeeName = this.employee.employeeName;
      this.employeeNameEng = this.employee.employeeNameEng;
      this.employeePw = this.employee.employeePw;
      this.employeePhone = this.employee.employeePhone;
      this.employeeEmail = this.employee.employeeEmail;
      this.employeeDivision = this.employee.employeeDivision;
      this.employeeTeam = this.employee.employeeTeam;
      this.employeePosition = this.employee.employeePosition;
    },
    changeCompany: function () {
      switch (this.employeeCompany) {
        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.toUpperCase() === MODE_MODIFY;
    },
    isSettingMode: function () {
      return this.mode.toUpperCase() === MODE_SETTING;
    },
    clickOk: function () {
      let path = "/api/admin-web/user";

      let jsonData = {
        employeeNo: this.employeeNo,
        registerEmployeeNo: "000000",
        updateEmployeeNo: "000000",

        employeeName: this.employeeName,
        employeeNameEng: this.employeeNameEng,
        employeePhone: this.employeePhone,
        employeeEmail: this.employeeEmail,
        employeePw: this.employeePw,
        employeeCompany: this.employeeCompany,
        employeeDivision: this.employeeDivision,
        employeeTeam: this.employeeTeam,
        employeePosition: this.employeePosition,
        usageExpDate: this.usageExpiryDate.replaceAll("-", ""),
        pwTrialState: "0",
        temporaryPwState: "N",
      };

      switch (this.mode.toUpperCase()) {
        case MODE_REGISTER:
          console.log("REGISTER PROCESS");

          this.$axios.post(path, jsonData).catch(function (error) {
            console.log("[ERR/REG]" + error);
          });
          break;
        case MODE_MODIFY:
        case MODE_SETTING:
          console.log(this.mode.toUpperCase() + " PROCESS");
          path += "/update/";

          this.$axios
            .put(path + this.employeeNo, jsonData)
            .catch(function (error) {
              console.log("[ERR/MOD]" + error);
            });
          break;
        default:
          // error
          console.log("[ERR/clickOk] Mode:" + this.mode);
          break;
      }

      this.$emit("finishProcess");
    },
    clickCancel: function () {
      console.log("press cancel button");
      this.$emit("finishProcess");
    },
  },
};
</script>

<style scoped></style>

 

변경 사항

<script>
...
const MODE_SETTING = "SETTING";

export default {
  ...
  mounted: function () {
    this.$nextTick(function () {
      console.log(this.mode.toUpperCase());
      switch (this.mode.toUpperCase()) {
        case MODE_MODIFY:
        case MODE_SETTING:  // 추가
          this.setData();
          break;
        ...
      }
    });
  },
clickOk: function () {
  ...
    case MODE_MODIFY:
    case MODE_SETTING:  // 추가
      ...
      this.$axios
        .put(path + this.employeeNo, jsonData)
        .catch(function (error) {
          console.log("[ERR/MOD]" + error);
        });
      break;
    ...
  }
}
  • MODE_MODIFY와 동일한 동작을 하도록 MODE_MODIFY 케이스에 MODE_SETTING 케이스 추가

 

src\components\common\UserPassword.vue

전체 코드를 확인하실 분들은 아래 더보기를 클릭해주세요.

더보기
<template>
  <v-container>
    <div class="text-h5 font-weight-medium ma-3">Password</div>

    <div class="ma-md-2">
      <v-row>
        <v-col>
          <v-text-field
            text
            outlined
            dense
            clearable
            :type="'password'"
            :rules="[rules.required, rules.password]"
            v-model="passwordCur"
            label="현재 비밀번호"
          ></v-text-field>
        </v-col>
      </v-row>

      <v-row>
        <v-col>
          <v-text-field
            text
            outlined
            dense
            clearable
            :type="'password'"
            :rules="[rules.required, rules.password]"
            v-model="passwordTobe1"
            label="변경할 비밀번호"
          ></v-text-field>
        </v-col>
        <v-col>
          <v-text-field
            text
            outlined
            dense
            clearable
            :type="'password'"
            :rules="[rules.required, rules.password]"
            v-model="passwordTobe2"
            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>
export default {
  props: ["employee"],
  data() {
    return {
      rules: {
        required: (value) => !!value || "Required.",
        password: (value) => {
          const pattern = /^(?=.*\d)(?=.*[a-z]).{8,}$/;
          return (
            pattern.test(value) ||
            "영어 소문자, 숫자를 포함하여 8자리 이상으로 구성되어야 합니다."
          );
        },
      },
      passwordCur: "",
      passwordTobe1: "",
      passwordTobe2: "",
    };
  },
  methods: {
    clickCancel: function () {
      this.passwordCur = "";
      this.passwordTobe1 = "";
      this.passwordTobe2 = "";
    },
    clickOk: function () {
      let path = "/api/admin-web/user/update";
      let jsonData = {
        employeeNo: this.employee.employeeNo,
        employeePw: this.passwordTobe1,
        registerEmployeeNo: "000000",
        updateEmployeeNo: "000000",
        pwTrialState: "0",
        temporaryPwState: "N",
      };
      
      if (this.passwordTobe1 === this.passwordTobe2) {
        this.$axios
          .put(path, jsonData)
          .then((response) => {
            if (response.statusText == "OK") {
              alert("비밀번호를 변경하였습니다.");
            }
          })
          .catch(function (error) {
            console.log("[ERR/UserPwChange]" + error);
            if (error.response.status == "417") {
              alert("이전에 사용한 비밀번호로 변경할 수 없습니다.");
            }
          });
      } else {
        this.alert("변경할 비밀번호가 서로 일치하지 않습니다.");
      }
    },
  },
};
</script>

<style></style>

 

변경 사항

<script>
export default {
  props: ["employee"],
  ...
  methods: {
    clickOk: function () {
      let path = "/api/admin-web/user/update";
      let jsonData = {
        employeeNo: this.employee.employeeNo,
        employeePw: this.passwordTobe1,
        registerEmployeeNo: "000000",
        updateEmployeeNo: "000000",
        pwTrialState: "0",
        temporaryPwState: "N",
      };

      if (this.passwordTobe1 === this.passwordTobe2) {
        this.$axios
          .put(path, jsonData)
          .then((response) => {
            if (response.statusText == "OK") {
              alert("비밀번호를 변경하였습니다.");
            }
          })
          .catch(function (error) {
            console.log("[ERR/UserPwChange]" + error);
            if (error.response.status == "417") {
              alert("이전에 사용한 비밀번호로 변경할 수 없습니다.");
            }
          });
      } else {
        this.alert("변경할 비밀번호가 서로 일치하지 않습니다.");
      }
    },
  },
}
  • clickOk(): 사용자 비밀번호 변경 REST API 연결 함수

 

src\views\admin-web\BaseAdminSettings.vue

전체 코드를 확인하실 분들은 아래 더보기를 클릭해주세요.

<template>
  <v-container>
    <PrivateInformation
      :mode="'Setting'"
      :employee="this.employee"
    ></PrivateInformation>
    <v-divider></v-divider>

    <UserPassword :employee="this.employee"></UserPassword>
  </v-container>
</template>

<script>
import PrivateInformation from "./../../components/admin-web/users/AdminUsersDetail.vue";
import UserPassword from "./../../components/common/UserPassword.vue";

export default {
  components: {
    PrivateInformation,
    UserPassword,
  },
  data() {
    return {
      employee: {},
    };
  },
  mounted: function () {
    this.$nextTick(function () {
      this.getUserInformation();
    });
  },
  methods: {
    getUserInformation: function () {
      // TODO: employeeNo를 로그인한 사용자의 직원 번호로 교체 필요
      let employeeNo = "123456";
      let path = "/api/admin-web/user/" + employeeNo;

      this.$axios
        .get(path)
        .then((response) => {
          this.employee = response.data;
        })
        .catch(function (error) {
          console.log("[ERR/getUserInformation]" + error);
        });
    },
  },
};
</script>

<style></style>

 

변경 사항

<template>

<template>
  <v-container>
    <PrivateInformation
      :mode="'Setting'"
      :employee="this.employee"
    ></PrivateInformation>
    ...

    <UserPassword :employee="this.employee"></UserPassword>
  </v-container>
</template>
  • 사용자 정보 영역과 비밀번호 영역에 로그인한 사용자 정보 전달

<script>

<script>
...
export default {
  ...
  data() {
    return {
      employee: {},
    };
  },
  mounted: function () {
    this.$nextTick(function () {
      this.getUserInformation();
    });
  },
  methods: {
    getUserInformation: function () {
      // TODO: employeeNo를 로그인한 사용자의 직원 번호로 교체 필요
      let employeeNo = "123456";
      let path = "/api/admin-web/user/" + employeeNo;

      this.$axios
        .get(path)
        .then((response) => {
          this.employee = response.data;
        })
        .catch(function (error) {
          console.log("[ERR/getUserInformation]" + error);
        });
    },
  },
};
</script>
  • data() → employee: 로그인한 사용자 정보를 담을 변수
  • mounted: 화면 로드시 로그인한 사용자 정보를 획득하여 설정하는 함수 호출
  • methods: 로그인한 사용자 정보를 DB에서 획득하도록 REST API와 연결된 함수
    • 현재는 특정 사용자의 직원 번호로 고정하였으나 향후 로그인 기능 구현시 변경될 예정

 

 


 

 

댓글