21.03.24 lamplight서비스 프로젝트(평점 추가, 출력 구현, 리뷰 삭제 기능 구현, directorProfilePage 추가, orderListPage에서 진행단계별 필터링 기능 구현)

2021. 3. 24. 22:22Vue.js/Spring & Vue APP 프로젝트(프론트엔드)

# NOTE

-리뷰[...]
 -작성, 리스팅까진 성공[ㅇ]
 -리스팅시 갯수제한..진행중[]
-평점[ㅇ]
typescript 기초
https://www.tutorialsteacher.com/typescript/typescript-number

vue 템플릿 모음
https://vuejsexamples.com/tag/rating/

# 주요 소스코드

<ReviewAddPage.vue>

<template>

  <TitleBar>후기/평점 작성 페이지</TitleBar>

  <section class="section section-review-write-form-box px-2">
    <div class="container mx-auto">
      <div class="px-6 py-6 bg-white rounded-lg shadow-md">
        <form v-if="globalShare.isLogined" v-on:submit.prevent="checkAndAddReview">
          <div title="평점" class="text-center h-24">
            <div class="btn-success">평점</div>
            <select ref="newRatingPointElRef" class="text-xl h-10 mt-2 w-1/2 border">
              <option value="1">1점</option>
              <option value="2">2점</option>
              <option value="3">3점</option>
              <option value="4">4점</option>
              <option value="5">5점</option>
            </select>
          </div>
          <div class="btn-success">후기 작성</div>
          <FormRow>
            <textarea ref="newReviewBodyElRef" class="form-row-input border" placeholder="내용을 입력해주세요."></textarea>
          </FormRow>
          <FormRow >
            <div class="btns">
              <input type="submit" value="작성" class="btn-primary" />
            </div>
          </FormRow>
        </form>
        <div v-else>
          <router-link class="btn-link" to="/member/login">로그인</router-link> 후 이용해주세요.
        </div>
      </div>
    </div>
  </section>


</template>

<script lang="ts">
import { defineComponent, reactive, ref, getCurrentInstance, onMounted } from 'vue'
import { MainApi } from '../apis'
import { Router } from 'vue-router'
import * as Util from '../utils'


export default defineComponent({
  name: 'ReviewAddPage',

  props: {
    globalShare: {
      type: Object,
      required: true
    },
    relId: {
      type: Number,
      required: true
    },
    relTypeCode: {
      type: String,
      required: true
    }
  },

  setup(props){
    const router:Router = getCurrentInstance()?.appContext.config.globalProperties.$router;
    const mainApi:MainApi = getCurrentInstance()?.appContext.config.globalProperties.$mainApi;

    const newReviewBodyElRef = ref<HTMLInputElement>();
    const newRatingPointElRef = ref<HTMLInputElement>();


    /* 공백 체크 */
    function checkAndAddReview(){

      //일반적으로 안해도 되지만 typescript에서는 해야됨
      //제목
      if(newReviewBodyElRef.value == null){
        return;
      }

      const newReviewBodyEl = newReviewBodyElRef.value;
      newReviewBodyEl.value = newReviewBodyEl.value.trim();

      if(newReviewBodyEl.value.length == 0){
        alert('내용을 입력해 주세요.')
        newReviewBodyEl.focus();
        return;
      }

    
    const raingAdd = (onSuccess:Function) => {

        mainApi.rating_doAdd(props.relTypeCode, props.relId, Util.toIntOrNull(newRatingPointElRef.value?.value), props.globalShare.loginedMember.id)
        .then(axiosResponse => {
          
          if ( axiosResponse.data.fail ) {
            alert(axiosResponse.data.msg);
            return;
          }
          else{
            onSuccess();
          }
        });
      };


      const startAddReview = () =>{
      // 작성 함수로 보내기
          addReview(props.relTypeCode, props.relId, newReviewBodyEl.value, props.globalShare.loginedMember.id);
      }

      raingAdd(startAddReview);
    }

    //typescript에서는 title:string, body:string 이런식으로 type을 적어주어야 한다
      function addReview(relTypeCode:string, relId:number, body:string, memberId:number){
       
        mainApi.review_doAdd(relTypeCode, relId, body, memberId)
        .then(axiosResponse => {
          alert(axiosResponse.data.msg);
          
          // 로그인이 fail 상태이면 리턴
          if ( axiosResponse.data.fail ) {
            return;
          }

          //authKey가 있는 상태에서 가능
          const newReviewId = axiosResponse.data.body.id;
          //alert(newArticleId + "번 게시물 등록 완료!!");

          router.replace("/director/list");
        });
      }

    return{
        newReviewBodyElRef,
        newRatingPointElRef,
        checkAndAddReview,
        
    }

  }
  
})
</script>

<style scoped>

</style>

<OrderListPage.vue>

<template>
  <!-- 메인-요청 리스트 시작 -->
  <section class="orderList-section bg-gray-500">
    <div class="h-2 bg-gray-500"></div>
    <!--리스트 search 시작-->
    <div class="flex h-10 orderList-section-search m-2 text-right rounded-md">
      <select ref="selectStepLevelElRef" class="h-full w-1/4 rounded-l-md" id="" @change="onChangeStepLevel">
        <option value="0">진행단계 전체</option>
        <option value="1">요청서 검토중</option>
        <option value="2">장례준비중</option>
        <option value="3">장례진행중</option>
        <option value="4">장례종료(결제대기중)</option>
        <option value="5">결제완료</option>
      </select>
      <select class="h-full w-1/4" id="" @change="onChangeSearchKeywordType($event)">
        <option value="title">제목</option>
        <option value="body">내용</option>
        <option value="funeralHome">장례식장</option>
      </select>
      <input id="searchKeywordElRef" ref="searchKeywordElRef" class="h-full w-full pl-4" type="text" placeholder="검색어 입력" :value="searchKeyword" @keyup.enter="onInput($event)" >
      <button class="w-20 text-white rounded-r-md bg-blue-500" type="button" @click="onClickInput">검색</button>
    </div>
    <!--리스트 search 끝-->
    <!--리스트 grid 시작-->
    <div class="orderList-section-grid grid grid-cols-1 md:grid-cols-3 gap-3 overflow-hidden">
      <!--리스트 grid__body 시작-->
      <div class="mt-6" v-bind:key="order.id" v-for="order in filteredOrders">
      <div class="orderList-section-grid__body p-8 bg-white m-2 border rounded-xl">
        <!--진행단계-->
        <div class="btn-success">
          진행단계: {{returnToString(order.stepLevel)}}
        </div>
        <!--제목-->
        <div class="text-center m-4">
          <router-link :to="'/order/detail?id=' + order.id" class="text-indigo-500 font-bold text-xl md:text-2xl hover:text-gray-700">
            {{ order.title }}
          </router-link>
        </div>
        <!--담당지도사-->
        <h1 class="font-semibold text-gray-900 leading-none text-xl mt-1 mb-3 capitalize break-normal">
          담당자회원번호: {{order.directorId}}
        </h1>
        <!--의뢰인명-->
        <h1 class="font-semibold text-gray-900 leading-none text-xl mt-1 mb-3 capitalize break-normal">
          의뢰인: {{order.extra__member}}
        </h1>
         <!--장례식장-->
        <h1 class="font-semibold text-gray-900 leading-none text-xl mt-1 mb-3 capitalize break-normal">
          장례식장: {{order.funeralHome}}
        </h1>
        <!--옵션-->
        <div class="max-w-full border mt-2 p-1 pl-2 rounded-md">
          옵션1
          <p class="text-base font-medium tracking-wide text-gray-600 mt-1">
            - 내용: {{ order.option1 }} / 수량: {{ order.option1qty }}
          </p>
        </div>
        <div class="max-w-full border mt-2 p-1 pl-2 rounded-md">
          옵션2
          <p class="text-base font-medium tracking-wide text-gray-600 mt-1">
            - 내용: {{ order.option2 }} / 수량: {{ order.option2qty }}
          </p>
        </div>
        <div class="max-w-full border mt-2 p-1 pl-2 rounded-md">
          옵션3
          <p class="text-base font-medium tracking-wide text-gray-600 mt-1">
            - 내용: {{ order.option3 }} / 수량: {{ order.option3qty }}
          </p>
        </div>
        <div class="max-w-full border mt-2 p-1 pl-2 rounded-md">
          옵션4
          <p class="text-base font-medium tracking-wide text-gray-600 mt-1">
            - 내용: {{ order.option4 }} / 수량: {{ order.option4qty }}
          </p>
        </div>
        <div class="max-w-full border mt-2 p-1 pl-2 rounded-md">
          옵션5
          <p class="text-base font-medium tracking-wide text-gray-600 mt-1">
            - 내용: {{ order.option5 }} / 수량: {{ order.option5qty }}
          </p>
        </div>
        <!--추가 요청 내용-->
        <div class="max-w-full border mt-2 p-1 pl-2 rounded-md">
          추가 요청사항
          <p class="text-base font-medium tracking-wide text-gray-600 mt-1">
            - {{ order.body }}
          </p>
        </div>
        <router-link :to="'/order/detail?id=' + order.id" class="block btn-primary mt-2 h-10 w-full rounded-md">
            상세보기
        </router-link>
        <router-link :to="'/review/doAdd?relTypeCode=director&relId=' + order.directorId" class="block btn-secondary mt-2 h-10 w-full rounded-md">
            후기/평점 작성
        </router-link>
        
      </div>
      </div>
      <!--리스트 grid__body 끝-->
    </div>
    <!--리스트 grid 끝-->
  </section>
  <!-- 메인-지도사 리스트 끝 -->

</template>

<script lang="ts">
import { defineComponent, reactive, ref, getCurrentInstance, onMounted, computed, watch } from 'vue'
import { IOrder } from '../types'
import { MainApi } from '../apis'

const searchKeywordElRef = ref<HTMLInputElement>();
const selectStepLevelElRef = ref<HTMLInputElement>();

export default defineComponent({
  name: 'OrderListPage',

//props 속성은 컴포넌트 간에 데이터를 전달할 수 있는 컴포넌트 통신 방법입니다. 
//props 속성을 기억할 때는 상위 컴포넌트에서 하위 컴포넌트로 내려보내는 데이터 속성으로 기억하면 쉽습니다
  props: {
    globalShare: {
      type: Object,
      required: true
    },
    memberId: {
      type: Number,
      required: true
    }
  },

  setup(props){
    const mainApi:MainApi = getCurrentInstance()?.appContext.config.globalProperties.$mainApi;

    const state = reactive({
      orders: [] as IOrder[],
      searchKeyword: '' as string,
      result:'' as string,
      selectStepLevel: 0,
    });

    let searchKeywordType = "title";

    function onInput(event:any){
      state.searchKeyword = event.target.value;
      return state.searchKeyword;
    }

    function onClickInput() {
      if(searchKeywordElRef.value == undefined){
        return;
      }

      state.searchKeyword = searchKeywordElRef.value.value;
      return state.searchKeyword;
    }

    function onChangeSearchKeywordType(event:any){
      searchKeywordType = event.target.value;
      return searchKeywordType;
    };


    function onChangeStepLevel() {

      if(selectStepLevelElRef.value == undefined){
        return;
      }
      state.selectStepLevel = parseInt(selectStepLevelElRef.value?.value);
      return state.selectStepLevel;
    }

    function returnToString(stepLevel:any) {
      let stepLevelToStr = ''; 
      if(stepLevel == 1){
        stepLevelToStr = '요청서 검토중';
      }
      if(stepLevel == 2){
        stepLevelToStr = '외뢰승인(장례준비중)';
      }
      if(stepLevel == 3){
        stepLevelToStr = '장례진행중';
      }
      if(stepLevel == 4){
        stepLevelToStr = '장례종료(결제대기중)';
      }
      if(stepLevel == 5){
        stepLevelToStr = '결제완료';
      }

      return stepLevelToStr;
    }

    //alert("1");
    const filteredOrders = computed(() => {
      //alert("2");
      state.result = state.searchKeyword;

      let filteredOrders = state.orders;
      
      //filteredOrders = state.orders.filter((order:IOrder) => order.stepLevel === state.selectStepLevel)
      if(state.selectStepLevel == 0){
        if(searchKeywordType == "title"){
        filteredOrders = state.orders.filter((order:IOrder) => order.title.includes(state.searchKeyword))
      }
      if(searchKeywordType == "body"){
        filteredOrders = state.orders.filter((order:IOrder) => order.body.includes(state.searchKeyword))
      }
      if(searchKeywordType == "funeralHome"){
        filteredOrders = state.orders.filter((order:IOrder) => order.funeralHome.includes(state.searchKeyword))
      }
      }
      else{
        if(searchKeywordType == "title"){
        filteredOrders = state.orders.filter((order:IOrder) => order.title.includes(state.searchKeyword) && order.stepLevel === state.selectStepLevel)
      }
      if(searchKeywordType == "body"){
        filteredOrders = state.orders.filter((order:IOrder) => order.body.includes(state.searchKeyword) && order.stepLevel === state.selectStepLevel)
      }
      if(searchKeywordType == "funeralHome"){
        filteredOrders = state.orders.filter((order:IOrder) => order.funeralHome.includes(state.searchKeyword) && order.stepLevel === state.selectStepLevel)
      }
      }

      return filteredOrders
    })

    const memberId = props.memberId;

    function loadOrders(memberId:number){
      //alert("4");
      mainApi.order_list(memberId)
      .then(axiosResponse => {
          state.orders = axiosResponse.data.body.orders;
      });

    }

    // onMounted 바로 실행하는 것이 아닌 모든 것이 준비되었을때 실행됨
    onMounted(() => {
      //alert("3");
      loadOrders(memberId);
    });


    return{
      state,
      filteredOrders,
      searchKeywordElRef,
      onInput,
      onClickInput,
      returnToString,
      onChangeSearchKeywordType,
      onChangeStepLevel,
      selectStepLevelElRef

    }

  }
})
</script>


<style scoped>
/* https://tailwindcomponents.com/component/blogs-posts-card */
.orderList-section-grid__body{
  min-width: 400px;
}

</style>