seokjin8678 / festago-manager-front Goto Github PK
View Code? Open in Web Editor NEW페스타고 백오피스 페이지
Home Page: https://festago-manager-front-sz2c-seokjin8678s-projects.vercel.app
페스타고 백오피스 페이지
Home Page: https://festago-manager-front-sz2c-seokjin8678s-projects.vercel.app
루트 어드민 페이지에 새로운 어드민 계정 생성, 서버 버전 조회, 테스트용 각 레벨 별 로그 생성 기능을 추가합니다.
학교 생성 페이지의 지역은 추가 되었지만, 학교 수정 페이지에서 지역이 추가 되지 않았기 때문에 수정 페이지에도 지역을 추가한다.
백엔드에서 관리자용 학교 조회 API 경로를 /admin/api/v1/schools
로 변경하였으므로, 학교 조회 시 요청하는 URL을 변경합니다.
woowacourse-teams/2023-festa-go#794
해당 PR 머지되면 진행
#86 PR에서 isLoading 변수를 없애기 위해 async 함수를 사용했는데, 반환 타입이 Promise<Promise<void | undefined>
가 되어서 타입스크립트 컴파일 에러가 발생함
따라서 콜백 메서드의 반환 타입을 any로 변경하여 컴파일이 되도록 변경
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
객체를 사용하여 간결하게 로직을 작성할 수 있음
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;
});
});
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
서버 URL이 다음과 같이 변경됨
festa-go.site -> festago.kro.kr
따라서 서버 URL을 변경함
아티스트 생성 시 backgroundImageUrl이 없어서 400 에러가 발생하고 있음
따라서 backgroundImageUrl을 추가할 것
백엔드 개발 서버가 세팅되었고, 백엔드 개발 서버에서 관리자 페이지 사용을 위해 .env.development
파일의 API URL을 개발 서버로 변경합니다.
<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"
/>
백엔드 로그인 API의 응답으로 다음과 같은 로그인 응답이 와야한다.
{
accessToken: string,
username: string,
authType: AuthType
}
하지만 지금은 로그인 응답이 accessToken
만 쿠키에 담아서 오므로 기능을 수행할 수 없다.
따라서 백엔드 로그인 API가 완성되기 전까지 임시로 권한을 Admin으로 고정한다.
class FestagoError implements Error {
name: string = ''; // 'FestagoError'
...
}
학교 조회에서 정렬이 되지 않는 버그가 확인됨
원인은 쿼리 파라미터로 정렬 조건을 보낼 때 sort=id,desc
형식이 아닌 sortBy=id&order=desc
로 보내기 때문으로 확인
학교 수정 페이지의 경우 /admin/school/edit/:id
와 같이 URL이 지정되어 있음
하지만 /admin/schools/1/edit
과 같은 형식이 명확하므로 URL을 변경하도록 함
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을 사용할 것이기 때문에 별도의 컴포넌트로 분리하여 재사용성 확보해야함
사용자가 요청을 보낼 때, 우연하게 토큰이 만료되는 시간과 겹쳐서 401 응답이 발생할 수 있다.
이때 사용자가 입력하던 내용이 사라질 수 있으므로 토큰이 만료된 후 갱신을 하는 것 보다, 만료되기 전 갱신을 해야한다.
예상 구현 방안
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>
지금은 기능의 학교 목록을 보여주는게 끝이지만, 추후 축제, 공연, 학생 등 같은 패턴의 구조가 나올 수 있기에 검색 폼과 데이터 테이블을 분리하여 재사용성을 높인다.
학교 도메인에 logoUrl, backgroundImageUrl 컬럼이 새롭게 추가되었으므로, 학교를 생성 할 때 해당 컬럼을 추가해야함
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.