Giter VIP home page Giter VIP logo

festago-manager-front's People

Contributors

seokjin8678 avatar

Watchers

 avatar

festago-manager-front's Issues

학교 조회 API URL을 수정한다.

백엔드에서 관리자용 학교 조회 API 경로를 /admin/api/v1/schools로 변경하였으므로, 학교 조회 시 요청하는 URL을 변경합니다.

수정/삭제 페이지에 사용되는 Form을 Component로 분리하여 재사용성을 확보한다.

School, Artist Edit 뷰에 다음과 같이 중복된 Form 형식이 나타나고 있음

<template>
  <v-dialog
    v-model="showDialog"
    max-width="500"
  >
    <v-card class="pa-5">
      <v-card-title class="text-h5 text-center">
        정말로 삭제할까요?
      </v-card-title>
      <v-card-actions>
        <v-spacer />
        <v-btn
          text="취소"
          color="blue-darken-1"
          variant="text"
          @click="showDialog = false"
        />
        <v-btn
          text="삭제"
          color="red-darken-1"
          variant="text"
          @click="deleteArtist"
        />
        <v-spacer />
      </v-card-actions>
    </v-card>
  </v-dialog>
  <v-card
    class="mx-auto pa-3 pa-md-15 py-8 mt-16 w-75"
    max-width="800"
    min-width="350"
    elevation="4"
  >
    <v-card-title class="mb-3 text-h4 text-center">
      아티스트 수정/삭제
    </v-card-title>
    <v-form
      v-model="invalidForm"
      @submit.prevent="onUpdateSubmit"
    >
      <v-text-field
        class="mb-3"
        variant="outlined"
        label="ID"
        :model-value="artistId"
        :readonly="true"
      />
      <v-text-field
        class="mb-3"
        v-model="nameField.value.value"
        :error-messages="nameField.errorMessage.value"
        placeholder="아티스트 이름"
        variant="outlined"
        label="아티스트 이름"
      />
      <v-text-field
        class="mb-3"
        v-model="profileImageField.value.value"
        :error-messages="profileImageField.errorMessage.value"
        placeholder="https://festa-go.site/image.png"
        variant="outlined"
        label="아티스트 이미지 URL"
      />
      <v-btn
        :disabled="!invalidForm"
        :loading="loading"
        class="text-h6"
        type="submit"
        text="수정"
        color="blue"
        :block="true"
      />
      <v-btn
        :disabled="loading"
        class="text-h6 mt-4"
        text="삭제"
        color="red"
        :block="true"
        @click="showDialog = true"
      />
      <v-btn
        :disabled="loading"
        class="text-h6 mt-4"
        text="취소"
        color="grey"
        :block="true"
        @click="$router.go(-1)"
      />
    </v-form>
  </v-card>
</template>

#44 이슈와 동일하게 별도의 컴포넌트로 분리하여 재사용성 확보해야함

수정 폼의 장황한 재설정 로직을 개선한다.

다음과 같이 수정폼에 초기 값을 설정하기 위해 장황한 resetField 함수를 호출하고 있음

onMounted(() => {
  AdminSchoolService.fetchOneSchool(parseInt(route.params.id as string)).then(response => {
    const { id, name, domain, region, logoUrl, backgroundImageUrl } = response.data;
    schoolId.value = id;
    nameField.resetField({
      value: name,
    });
    domainField.resetField({
      value: domain,
    });
    regionField.resetField({
      value: region,
    });
    logoUrlField.resetField({
      value: logoUrl,
    });
    backgroundImageUrlField.resetField({
      value: backgroundImageUrl,
    });
  }).catch(e => {
    if (e instanceof FestagoError) {
      router.push(RouterPath.Admin.AdminSchoolManageListPage.path);
      snackbarStore.showError('해당 학교를 찾을 수 없습니다.');
    } else throw e;
  });
});

하지만 useForm 객체의 resetForm 함수를 사용하면 다음과 같이 한 번에 재설정 처리를 할 수 있음

onMounted(() => {
  schoolId.value = parseInt(route.params.id as string);
  AdminSchoolService.fetchOneSchool(schoolId.value).then(response => {
    resetForm({ values: response.data });
  }).catch(e => {
    if (e instanceof FestagoError) {
      router.push(RouterPath.Admin.AdminSchoolManageListPage.path);
      snackbarStore.showError('해당 학교를 찾을 수 없습니다.');
    } else throw e;
  });
});

const { resetForm } = useForm<UpdateSchoolRequest>({
    ...
}

이 경우 서버에서 받은 응답이 폼의 형식과 동일해야 하므로, 일부 수정 폼은 한 번에 적용할 수 없지만, resetField를 여러 번 호출하는 것 보다 가독성이 높아지고 중복 코드가 사라질 것으로 예상됨

또한 수정 요청을 보낼 때, 폼이 수정되었는지 확인하는 로직을 meta 객체를 사용하여 간결하게 로직을 작성할 수 있음

AS-IS

const onUpdateSubmit = handleSubmit(request => {
  loading.value = true;
  setTimeout(() => (loading.value = false), 1000);
  if (allTrue(
    !isFieldDirty('name'),
    !isFieldDirty('domain'),
    !isFieldDirty('region'),
    !isFieldDirty('logoUrl'),
    !isFieldDirty('backgroundImageUrl'),
  )) {
    snackbarStore.showError('아무것도 수정되지 않았습니다.');
    return;
  }
  AdminSchoolService.updateSchool(schoolId.value!, request).then(() => {
    loading.value = false;
    snackbarStore.showSuccess('학교가 수정되었습니다.');
    nameField.resetField({
      value: nameField.value.value,
    });
    domainField.resetField({
      value: domainField.value.value,
    });
    regionField.resetField({
      value: regionField.value.value,
    });
    logoUrlField.resetField({
      value: logoUrlField.value.value,
    });
    backgroundImageUrlField.resetField({
      value: backgroundImageUrlField.value.value,
    });
  }).catch(e => {
    if (e instanceof FestagoError) {
      snackbarStore.showError(e.message);
    } else throw e;
  });
});

TO-BE

const onUpdateSubmit = handleSubmit(request => {
  if (!meta.value.dirty) {
    snackbarStore.showError('아무것도 수정되지 않았습니다.');
    return;
  }
  loading.value = true;
  setTimeout(() => (loading.value = false), 1000);
  AdminSchoolService.updateSchool(schoolId.value!, request).then(() => {
    loading.value = false;
    snackbarStore.showSuccess('학교가 수정되었습니다.');
    resetForm({ values: request });
  }).catch(e => {
    if (e instanceof FestagoError) {
      if (e.isValidError()) setErrors(e.result);
      snackbarStore.showError(e.message);
    } else throw e;
  });
});

참조

https://vee-validate.logaretm.com/v4/guide/composition-api/handling-forms/#form-metadata

fix: DataTable에 누락된 model을 추가한다.

<DataTable
  v-model="loading" // <-- 누락
  :table-headers="tableHeaders"
  :items-per-page-options="itemsPerPageOptions"
  :fetch="fetch"
  :item-length="items.schools.length"
  :items="items.schools"
  :detail-page-router-name="RouterPath.Admin.AdminSchoolManageEditPage.name"
/>

생성 페이지에 사용되는 Form을 Component로 분리하여 재사용성을 확보한다.

School, Artist 생성 뷰에 다음과 같이 중복된 Form 형식이 나타나고 있음

<template>
  <v-card
    class="mx-auto pa-3 pa-md-15 py-8 mt-16 w-75"
    max-width="800"
    min-width="350"
    elevation="4"
  >
    <v-card-title class="mb-3">
      <p class="text-h4 text-center">
        아티스트 추가
      </p>
    </v-card-title>
    <v-form
      v-model="invalidForm"
      @submit.prevent="onSubmit"
    >
      <v-text-field
        class="mb-3"
        v-model="nameField.value.value"
        :error-messages="nameField.errorMessage.value"
        placeholder="아티스트 이름"
        variant="outlined"
        label="아티스트 이름"
      />
      <v-text-field
        class="mb-3"
        v-model="profileImageField.value.value"
        :error-messages="profileImageField.errorMessage.value"
        placeholder="https://festa-go.site/image.png"
        variant="outlined"
        label="아티스트 이미지 URL"
      />
      <v-btn
        :disabled="!invalidForm"
        :loading="loading"
        class="text-h6"
        type="submit"
        text="생성"
        color="blue"
        :block="true"
      />
    </v-form>
  </v-card>
</template>

추후 Festival, Stage 등 여러 뷰에도 같은 형식의 Form을 사용할 것이기 때문에 별도의 컴포넌트로 분리하여 재사용성 확보해야함

JWT 토큰이 만료되기 전, 핸들링을 수행한다.

사용자가 요청을 보낼 때, 우연하게 토큰이 만료되는 시간과 겹쳐서 401 응답이 발생할 수 있다.
이때 사용자가 입력하던 내용이 사라질 수 있으므로 토큰이 만료된 후 갱신을 하는 것 보다, 만료되기 전 갱신을 해야한다.

예상 구현 방안

  • router의 navigtion guard를 사용하여 토큰의 만료 시간과 현재 시간을 비교하여, n분 이하이면 토큰 재갱신을 한다.
  • 재갱신은 백엔드의 API를 이용한다.
  • 엑세스 토큰과 리프레쉬 토큰을 보내고, 백엔드에서 갱신된 엑세스 토큰, 리프레쉬 토큰을 반환한다.
  • 2023/1/15 기준, 리프레쉬 토큰이 구현되어 있지 않으므로 리프레쉬 토큰을 구현해야 한다.

재사용을 위해 AdminSchoolManageListView의 검색폼과 데이터 테이블을 컴포넌트로 분리한다.

AdminSchoolManageListView의 템플릿 코드는 다음과 같다.

<template>
  <v-card
    class="pa-10 ma-10 pt-0"
    :flat="true"
    title="학교 목록"
  >
    <v-row :no-gutters="true" align="center">
      <v-col :cols="3">
        <v-select
          v-model="searchRequest.filterKeyword"
          class="pa-2 ma-2"
          :clearable="true"
          label="필터"
          :items="searchFilters"
          variant="outlined"
          :hide-details="true"
        />
      </v-col>
      <v-col :cols="8">
        <v-text-field
          class="pa-2 ma-2"
          v-model="searchRequest.searchKeyword"
          label="Search"
          prepend-inner-icon="mdi-magnify"
          :single-line="true"
          variant="outlined"
          :hide-details="true"
          maxLength="50"
        />
      </v-col>
      <v-col :cols="1">
        <v-btn
          class="py-7 text-h6"
          color="blue"
          variant="flat"
          :block="true"
          @click="searchResult"
          text="검색"
        />
      </v-col>
    </v-row>
    <v-data-table-server
      :headers="tableHeaders"
      :items-length="totalItems"
      :items="items.schools"
      :loading="loading"
      :items-per-page-options="itemsPerPageOption"
      v-model:items-per-page="itemsPerPage"
      @update:options="fetchItems"
    >
      <template v-slot:item.actions="{item}">
        <v-icon
          class="mr-2"
          icon="mdi-pencil"
          color="grey-darken-3"
          @click="$router.push({
            name: RouterPath.Admin.AdminSchoolManageEditPage.name,
            params: { id: item.id }
          })"
        />
      </template>
    </v-data-table-server>
  </v-card>
</template>

지금은 기능의 학교 목록을 보여주는게 끝이지만, 추후 축제, 공연, 학생 등 같은 패턴의 구조가 나올 수 있기에 검색 폼과 데이터 테이블을 분리하여 재사용성을 높인다.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.