본문 바로가기
Programming/Vue

[실습] 뷰(Vue)로 설정 화면 구현하기 - UI/Layout 구현하기

by 돌방로그 2023. 3. 2.


오늘의 실습 목표는 "설정 화면 만들어보기!" 입니다.
본 게시글에서 다루는 사항은 설정 화면의 UI/Layout을 구현하는 과정입니다.

 


설정 화면 구현하기 - UI/Layout

사전 준비

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

 

 


결과 이미지

구현하기에 앞서 본 글을 따라하시면 아래와 같이 화면의 레이아웃을 구성하실 수 있습니다.

 


소스 코드

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="company"
            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="nameKor"
            label="국문이름"
            placeholder="홍길동"
          ></v-text-field>
        </v-col>
        <v-col>
          <v-text-field
            text
            outlined
            dense
            clearable
            :disabled="isSettingMode()"
            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-show="!isSettingMode() && !isModifyMode()">
          <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
            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
      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.password = this.employee.password;
      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.toUpperCase() === MODE_MODIFY;
    },
    isSettingMode: function () {
      return this.mode.toUpperCase() === MODE_SETTING;
    },
    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",

              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/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>

 

본 코드를 재사용하여 설정 화면에서도 사용할 수 있도록 아래에 대해서만 일부 수정하였습니다.

<template>

...
<div class="text-h5 font-weight-medium ma-2">
  {{ mode }}
</div>

<div class="ma-md-2">
  <v-row>
    <v-col>
      <v-combobox
        ...
        :disabled="isModifyMode() || isSettingMode()"
        label="회사"
      ></v-combobox>
    </v-col>
    <v-col>
      <v-text-field
        ...
        :disabled="isModifyMode() || isSettingMode()"
        label="직원번호"
      ></v-text-field>
    </v-col>
  </v-row>
  <v-row>
    <v-col>
      <v-text-field
        ...
        :disabled="isSettingMode()"
        label="국문이름"
      ></v-text-field>
    </v-col>
    <v-col>
      <v-text-field
        ...
        :disabled="isSettingMode()"
        label="영문이름"
      ></v-text-field>
    </v-col>
  </v-row>

  ...

  <v-row>
    <v-col v-show="!isSettingMode() && !isModifyMode()">
      <v-text-field
        ...
        label="비밀번호"
      ></v-text-field>
    </v-col>
  </v-row>

  ...

  <v-row>
    ...
    <v-text-field
      ...
      v-show="!isSettingMode()""
      label="사용기한일자"
    ></v-text-field>
    ...
  </v-row>
  ...
</div>
...
  • 기존 구현한 관리자 포털 사용자 추가/수정 화면에 추가적으로 "설정" 화면인 경우에만 Hide 혹은 Disable되는 필드에 대해서 조건에 맞게 사용자에게 보여지도록 구현
    • Hide: 비밀번호, 사용기한 일자
    • Disable: 회사, 직원번호, 직원 국문이름, 직원 영문이름

 

<script>

<script>
const MODE_SETTING = "SETTING";

export default {
  ...
  methods: {
    ...
    isSettingMode: function () {
      return this.title.toUpperCase() === MODE_SETTING;
    },
    ...
  }
};
</script>
  • 전역 변수(MODE_SETTING): 설정 화면에 대한 제어 모드 추가 정의
  • isSettingMode (): 현재 오픈된 화면의 모드가 설정 화면인지 확인하는 함수

 

src\components\common\UserPassword.vue

로그인한 사용자의 비밀번호 변경 컴포넌트 파일입니다.

기존에 구현한 'AdminUsersDetail.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 {
  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 () {
      // TODO: SOMETHINGS
    },
  },
};
</script>

<style></style>

 

src\views\admin-web\BaseAdminSettings.vue

메뉴/사이드바의 라우팅 결과로 표시될 최상위 컴포넌트 파일입니다.

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

    <UserPassword></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,
  },
};
</script>

<style></style>
  • PrivateInformation: 개인 정보 표시/수정 영역 컴포넌트
    • 'AdminUsersDetail' 컴포넌트를 재사용함에 따른 title을 props로 전달
  • UserPassword: 로그인한 사용자의 비밀번호 변경 컴포넌트

 


댓글