Front-End

[vue.js+vuetify.js] modal dialog( 모달 창, 팝업창)를 만드는 샘플 예제 코드

기존에 사용중인 것이 아닌 UI가 변경된 다이얼로그 창 ( alert 창 ) 을 하나 새롭게 만들어야하는 상황이 왔다. 기획서 작성하는 분에게 , 기존 UI를 벗어난 기획은 개발시간을 늘리는 지름길이라고 얘기했고, 웹 퍼블리싱 작업이 필요하고 퍼블리셔가 해야할 일이며, “나의 손을 벗어난다” 까지 얘기를 했는데, 기획서를 받아보니 ㅋㅋㅋㅋㅋ 

그럼 나도 내가 해야할일만 만들어줄게 라는 생각으로 모든 기능을 완벽히 구현하였다. 그리고 보란듯이 ui 기능만 잘되는 UI로 만들었다. 그러는 과정에 모달 팝업창과 같은 성격으로 다이얼로그창을 생성해야하는데, 기존에 사용중인 다이얼로그 창은 사용할 수 없는 구조라 새롭게 만들어야했다. 

사실 모달 다이얼로그창이라고 해봐야 다이얼로그 창이 떴을 때 뒷 배경을 흐리게 하는 레이어 창을 하나 만들어서 모든 텍스트와 버튼을 클릭할 수 없게 막아버리는게 핵심이다.  그러나 나는 CSS는 할 줄 모르고, 퍼블리셔도 아니고……열심히 또 정보를 찾아해맸고, 3개의 예제를 보고 난 후에야 제대로 된 기능을 구현할 수 있었다.

 

CustomDialog.vue

<template>
  <transition>
    <div class="modal-mask">
      <div class="modal-wrapper">
        <!-- <div class="modal-container"> -->
  <!--[이벤트 대상 고객 안내 팝업]-->
  <v-dialog
    v-model="visible"
    content-class="test-alert"
    max-width="295"
    hide-overlay
  >
    <v-card>
      <v-card-text>
        <v-badge
        content="알림"
        color="cyan darken-1"></v-badge>
        <v-btn class="test-dialog btn-close" style="margin-left:100px;"
          icon
          color="#222"
          @click="close()">
          <i class="test-icon test-icon-close-black"></i>
        </v-btn>
      </v-card-text>
      <v-card-text>
        <!-- <p class="txt-black" v-html="param.titleCont"></p> -->
        <div class="l20">
          <p>
            현재 ({{ contentTitle }}) 이벤트 대상자입니다. 등록하시겠습니까?
          </p>
        </div>
      </v-card-text>
          <v-card-text v-if="isShow">
            <!-- <v-row>
              <v-col class="pb0"><p>{{data.choicedIdx}}</p></v-col>
            </v-row> -->
            <v-list subheader>
              <v-list-item-group
                v-model="choicedIdx"
                multiple
              >
              <v-subheader>(이벤트 영업 현황)</v-subheader>
                <v-list-item
                  v-for="(item, i) in dataArray.items"
                  :key="`event-${i}`">
                  <template v-slot:default="{ active, }">
                    <v-list-item-action>
                      <v-checkbox
                        :input-value="active"
                        color="primary"
                      ></v-checkbox>
                    </v-list-item-action>
                    <v-list-item-content>
                      <v-list-item-title v-text="item.ITEM_NM"></v-list-item-title>
                    </v-list-item-content>
                  </template>
                </v-list-item>
              </v-list-item-group>
            </v-list>
          </v-card-text>
      <v-card-actions>
        <v-btn class="test-btn test-btn-primary" color="primary" @click="nextAction();"
        v-show="!isShow">예</v-btn>
        <v-btn class="test-btn" color="#f2f2f2" @click="secondSubmit();" v-show="!isShow">아니오</v-btn>
        <v-btn class="test-btn test-btn-primary" color="primary" @click="submit();"
        v-show="isShow">확인</v-btn>
      </v-card-actions>
    </v-card>
    <!-- Alert 다이얼로그 -->
    <AlertDialog :parentData="alertData" />
  </v-dialog>
        </div>
      </div>
    <!-- </div> -->
  </transition>
</template>

<script>
export default {
  name: 'CustomDialog',
  props: {
    visible: {
      type: Boolean,
    },
    contentTitle: {
      type: String,
      default: '"현재( )이벤트 대상자입니다. 등록하시겠습니까?',
    },
    guestID: {
      type: String,
    },
  },
  data: () => ({
    param: {
      titleCont: '',
    },
    choicedIdx: {},
    dataArray: {},
    isShow: false,
    totCnt: 0,
    single: {
      strItemIds: '',
      strItemNames: '',
    },
    // data: {
    //   choicedIdx: {},
    //   dataArray: {},
    // },
  }),
  updated() {
    console.log('####CustomDialog updated()');
  },
  mounted() {
    console.log('####CustomDialog mounted()');
  },
  methods: {
    close() {
      this.isShow = false;
      this.$emit('close', '');
    },
    secondSubmit() {
      this.isShow = false;
      this.$emit('close', 'PRIVIOUS');
    },
    submit() {
      // console.log(`##this.choicedIdx = ${this.choicedIdx.length}`);
      if (this.choicedIdx.length === undefined || this.choicedIdx.length === 0) {
          this.openAlertDialog({
            status: 'VALIDATION',
            body: '아이템을 선택하세요.',
          });
        return;
      }
      this.isShow = false;
      let strItemIds = '';
      let strItemNames = '';
      this.choicedIdx.sort();
      this.choicedIdx.forEach((item) => {
        strItemIds += `${this.dataArray.items[item].ITEM_ID},`;
        strItemNames += `${this.dataArray.items[item].ITEM_NM},`;
      });
      /* remove last character (',') */
      if (this.choicedIdx.length > 0) {
        strItemIds = strItemIds.slice(0, -1);
      }
      if (strItemNames.length > 0) {
        strItemNames = strItemNames.slice(0, -1);
      }

      this.$emit('close', 'PROMOTION', strItemIds, strItemNames);
    },
    initData() {
      this.choicedIdx = {};
      this.dataArray = {};
    },
    nextAction() {
      this.initData();
      this.searchPromotion();
    },
    searchPromotion() {
      // console.log('#searchPromotion#');
      // alert(this.guestID);
      const params = {
        ac: 'CONTROLLER_ACT001',
        workType: 'Q_GET_ITEM_LIST',
        contactId: this.guestID,
      };

      this.axiosCall(params).then((rs) => {
        if (rs.data.errorCode === 'MSG0001' || rs.data.errorCode === 'MSG0006') {
          const clsRow = rs.data.resultList;
          const rowCnt = rs.data.rowCount;
          const items = clsRow[0];
          this.totCnt = rowCnt;
          console.log(`###cnt = ${rowCnt}`);
          if (rowCnt === 1) {
            this.isShow = false;
            this.single.strItemIds = items[0].ITEM_ID;
            this.single.strItemNames = items[0].ITEM_NM;
            // console.log(`this.single.strItemIds: ${this.single.strItemIds}`);
            // console.log(`this.single.strItemNames: ${this.single.strItemNames}`);
            this.$emit('close', 'PROMOTION', this.single.strItemIds, this.single.strItemNames);
          } else {
            this.isShow = true;
            this.dataArray.items = items;
            this.$forceUpdate();
            // console.log(JSON.stringify(this.data.dataArray));
          }
        }
      });
    },
  },
};
</script>
<style>
  .modal-mask {
    position: fixed;
    z-index: 9998;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, .5);
    display: table;
    transition: opacity .3s ease;
    }

    .modal-wrapper {
    display: table-cell;
    vertical-align: middle;
    }

    .modal-header h3 {
    margin-top: 0;
    color: #42b983;
    }

    .modal-body {
    margin: 20px 0;
    }

    .modal-default-button {
    float: right;
    }

    .modal-enter {
    opacity: 0;
    }

    .modal-leave-active {
    opacity: 0;
    }
</style>

모달창을 호출하는 상위(메인 콤포넌트) Main.vue

<template>
  <v-container>
    
    <v-layout class="test-list-header pb10 pt10" row wrap>
      <v-flex>
        <p class="result">검색결과<em>{{page.totCnt+'건'}}</em></p>
      </v-flex>
    </v-layout>
 .........생략.............
    <div v-if="page.totCnt !== myItem.list.length"
      class="text-center mt15 mb14"
      @click="morePage();">
      <v-btn
        class="test-btn-more-round" rounded outlined color="#222">
      더보기 +
      </v-btn>
    </div>

    <!--플로팅 버튼-->
    <v-btn
      class="test-function-btn"
      bottom
      color="#222222"
      dark
      fab
      fixed
      right
      @click="editAdd();"
    >
      <v-icon>mdi-plus</v-icon>
    </v-btn>
    <AlertDialog :parentData="alertData" />

    <!--[이벤트대상 고객 안내 팝업]-->
    <CustomDialog v-if="eventGuideDialogIsShow"
      title="현재()이벤트 대상자입니다. 등록하시겠습니까?"
      :visible="eventGuideDialogIsShow"
      :contentTitle="event.eventConts"
      :contId="event.pGuestId" @close="closeEventGuideDialog" />
  </v-container>
</template>

<script>
import moment from 'moment';
import CustomDialog from '@/views/dialog/CustomDialog.vue';

export default {
  name: 'Test',
  components: {
    CustomDialog,
  },
  data: () => ({
    page: {
      totCnt: 0, // 리스트 전체 갯수
      rowPerPage: 10, // 최초 가지고 올 데이터 갯수
      currPage: 1, // 현재 페이지
    },
    cont: {
      list: [],
      contextMenu: [
        { id: 'add', name: '추가' },
        { id: 'remove', name: '제외' },
      ],
    },
    query: {
      searchStr: '',
    },
    eventGuideDialogIsShow: false,
    event: {
      eventConts: '',
      pGuestId: '',
      pCompanyId: '',
      pActionId: '',
    },
  }),  
  created() {
    this.searchMore();
  },
  mounted(){
	this.openCustomDialog();
  },
  methods: {
    morePage() {
      this.page.currPage += 1;
      this.searchMore('MORE');
    },
    async searchMore(isMore) {
    },
    openCustomDialog() {
      this.eventGuideDialogIsShow = true;
    },
    async closeEventGuideDialog(isVal, strEvent, strEventNames) {
      const curDate = moment(new Date()).format('YYYY-MM-DD');
      console.log(`strEvent: ${strEvent} , strEventNames: ${strEventNames}`);
      if (isVal === 'PROMOTION') {
          const param = {
            actId: '',
            eventIds: strEvent,
          };
          // console.log(`eventsIds : ${this.query.eventIds}`);
          this.$router.push({ name: 'EventDetail', params: param, query: { salt: moment(new Date()).format('YYYYMMDDhhmmss') } });
      } else if (isVal === 'PRIVIOUS') {
        this.moveNextPage();
      }
      this.eventGuideDialogIsShow = false;
    },
  },
};
</script>

Vue.js에서 제공하는 문서의 샘플코드의 CSS를 그대로 사용하여 모달 다이얼로그 구현을 하였다.

여기서 핵심은 CSS이다. 

 

[더 심플하게 처리 가능한 방법]

혹시 <v-dialog>를 사용중이라면  태그 선언 부분에 hide-overlay 가 있는지 살펴보고 있다면 제거하라.

그렇게 하면 모달 다이얼로그는 아니지만 배경을 흐리게 해주는 효과가 발생하였다.

 

AS-IS : 모달이 아닌 다이얼로그창

      <v-dialog
        v-model="isShow"
        content-class="sample-alert"
        max-width="295"
        hide-overlay
      >

TO-BE

      <v-dialog
        v-model="isShow"
        content-class="sample-alert"
        max-width="295"
      >

[REFERENCE]

 

Leave a Reply

error: Content is protected !!