Compare commits
3
Commits
b067d3547a
...
00e6ba90f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e6ba90f6 | ||
|
|
596aab54b6 | ||
|
|
87d9121ca9 |
@@ -7,6 +7,7 @@
|
|||||||
"serve": "vue-cli-service serve --port 8888",
|
"serve": "vue-cli-service serve --port 8888",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"build_development": "vue-cli-service build --mode development",
|
"build_development": "vue-cli-service build --mode development",
|
||||||
|
"test:gift": "node tests/gift.test.js && node tests/gift_category.test.js && node tests/gift_size.test.js",
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
const baseUrl = '/api/v2/admin/gifts'
|
||||||
|
|
||||||
|
async function getGifts({ status, applicationNo, nickname, page = 0, size = 20 } = {}) {
|
||||||
|
const params = { page, size }
|
||||||
|
if (status) params.status = status
|
||||||
|
if (applicationNo) params.applicationNo = applicationNo
|
||||||
|
if (nickname) params.nickname = nickname
|
||||||
|
const response = await Vue.axios.get(baseUrl, { params })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGift(applicationNo) {
|
||||||
|
const response = await Vue.axios.get(`${baseUrl}/${encodeURIComponent(applicationNo)}`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function arriveMailbox(applicationNo) {
|
||||||
|
const response = await Vue.axios.post(`${baseUrl}/${encodeURIComponent(applicationNo)}/arrive-mailbox`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeInspection(applicationNo) {
|
||||||
|
const response = await Vue.axios.post(`${baseUrl}/${encodeURIComponent(applicationNo)}/complete-inspection`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeDelivery(applicationNo) {
|
||||||
|
const response = await Vue.axios.post(`${baseUrl}/${encodeURIComponent(applicationNo)}/complete-delivery`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markUndeliverable(applicationNo, reason) {
|
||||||
|
const response = await Vue.axios.post(`${baseUrl}/${encodeURIComponent(applicationNo)}/mark-undeliverable`, { reason })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getGifts, getGift, arriveMailbox, completeInspection, completeDelivery, markUndeliverable }
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import { buildGiftCategoryRequest } from '@/utils/gift_category'
|
||||||
|
|
||||||
|
const baseUrl = '/api/v2/admin/gift-categories'
|
||||||
|
|
||||||
|
async function getGiftCategories() {
|
||||||
|
const response = await Vue.axios.get(baseUrl)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createGiftCategory(form) {
|
||||||
|
const response = await Vue.axios.post(baseUrl, buildGiftCategoryRequest(form))
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGiftCategory(categoryId, form) {
|
||||||
|
const response = await Vue.axios.put(`${baseUrl}/${encodeURIComponent(categoryId)}`, buildGiftCategoryRequest(form))
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deactivateGiftCategory(categoryId) {
|
||||||
|
const response = await Vue.axios.delete(`${baseUrl}/${encodeURIComponent(categoryId)}`)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getGiftCategories, createGiftCategory, updateGiftCategory, deactivateGiftCategory }
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import { giftSizes, buildGiftSizeRequest } from '@/utils/gift_size'
|
||||||
|
|
||||||
|
const baseUrl = '/api/v2/admin/gift-size-prices'
|
||||||
|
|
||||||
|
async function getGiftSizePrices() {
|
||||||
|
const response = await Vue.axios.get(baseUrl)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGiftSizePrice(sizeCode, form) {
|
||||||
|
if (!giftSizes.includes(sizeCode)) throw new Error('올바른 선물 사이즈가 아닙니다.')
|
||||||
|
const response = await Vue.axios.put(`${baseUrl}/${sizeCode}`, buildGiftSizeRequest(form))
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getGiftSizePrices, updateGiftSizePrice }
|
||||||
@@ -104,6 +104,33 @@ export default {
|
|||||||
|
|
||||||
// ADMIN 권한 전용 추가 메뉴들
|
// ADMIN 권한 전용 추가 메뉴들
|
||||||
if (role === 'ADMIN') {
|
if (role === 'ADMIN') {
|
||||||
|
const giftMenu = this.items.find(m => m && m.title === '선물함 관리')
|
||||||
|
if (giftMenu) {
|
||||||
|
if (!Array.isArray(giftMenu.items)) {
|
||||||
|
this.$set(giftMenu, 'items', giftMenu.items ? [].concat(giftMenu.items) : [])
|
||||||
|
}
|
||||||
|
if (!giftMenu.items.some(item => item && item.route === '/gift/list')) {
|
||||||
|
giftMenu.items.push({ title: '선물함 리스트', route: '/gift/list', items: null })
|
||||||
|
}
|
||||||
|
if (!giftMenu.items.some(item => item && item.route === '/gift/category')) {
|
||||||
|
giftMenu.items.push({ title: '선물 카테고리', route: '/gift/category', items: null })
|
||||||
|
}
|
||||||
|
if (!giftMenu.items.some(item => item && item.route === '/gift/size')) {
|
||||||
|
giftMenu.items.push({ title: '선물 사이즈', route: '/gift/size', items: null })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.items.push({
|
||||||
|
title: '선물함 관리',
|
||||||
|
route: null,
|
||||||
|
icon: 'mdi-gift',
|
||||||
|
items: [
|
||||||
|
{ title: '선물함 리스트', route: '/gift/list', items: null },
|
||||||
|
{ title: '선물 카테고리', route: '/gift/category', items: null },
|
||||||
|
{ title: '선물 사이즈', route: '/gift/size', items: null }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// '시리즈 관리' 메뉴에 '배너 등록' 하위 메뉴 추가
|
// '시리즈 관리' 메뉴에 '배너 등록' 하위 메뉴 추가
|
||||||
try {
|
try {
|
||||||
const seriesMenu = this.items.find(m => m && m.title === '시리즈 관리')
|
const seriesMenu = this.items.find(m => m && m.title === '시리즈 관리')
|
||||||
|
|||||||
@@ -362,6 +362,21 @@ const routes = [
|
|||||||
name: 'CharacterCalculate',
|
name: 'CharacterCalculate',
|
||||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCalculateList.vue')
|
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCalculateList.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/gift/list',
|
||||||
|
name: 'GiftList',
|
||||||
|
component: () => import(/* webpackChunkName: "gift" */ '../views/Gift/GiftList.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/gift/category',
|
||||||
|
name: 'GiftCategory',
|
||||||
|
component: () => import(/* webpackChunkName: "gift" */ '../views/Gift/GiftCategory.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/gift/size',
|
||||||
|
name: 'GiftSize',
|
||||||
|
component: () => import(/* webpackChunkName: "gift" */ '../views/Gift/GiftSize.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/original-work',
|
path: '/original-work',
|
||||||
name: 'OriginalList',
|
name: 'OriginalList',
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export const giftStatuses = [
|
||||||
|
{ text: '전체', value: '' },
|
||||||
|
{ text: '접수 완료', value: 'RECEIVED' },
|
||||||
|
{ text: '발송 확인', value: 'TRACKING_REGISTERED' },
|
||||||
|
{ text: '사서함 도착', value: 'ARRIVED_AT_MAILBOX' },
|
||||||
|
{ text: '검수 완료', value: 'INSPECTION_COMPLETED' },
|
||||||
|
{ text: '전달 완료', value: 'DELIVERED' },
|
||||||
|
{ text: '전달 불가', value: 'UNDELIVERABLE' },
|
||||||
|
{ text: '신청 취소', value: 'CANCELED' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const giftActions = [
|
||||||
|
{ value: 'ARRIVE_MAILBOX', label: '사서함 도착 처리', method: 'arriveMailbox' },
|
||||||
|
{ value: 'COMPLETE_INSPECTION', label: '검수 완료 처리', method: 'completeInspection' },
|
||||||
|
{ value: 'COMPLETE_DELIVERY', label: '전달 완료 처리', method: 'completeDelivery' },
|
||||||
|
{ value: 'MARK_UNDELIVERABLE', label: '전달 불가 처리', method: 'markUndeliverable' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export function validateUndeliverableReason(reason) {
|
||||||
|
if (typeof reason !== 'string' || !reason.trim()) return '전달 불가 사유를 입력해 주세요.'
|
||||||
|
if (reason.length > 255) return '전달 불가 사유는 255자 이내로 입력해 주세요.'
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
export function createGiftCategoryForm() {
|
||||||
|
return {
|
||||||
|
classificationNumber: '',
|
||||||
|
categoryCode: '',
|
||||||
|
name: '',
|
||||||
|
receiptCode: '',
|
||||||
|
representativeItem: '',
|
||||||
|
requiresDamageWaiver: false,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGiftCategoryRequest(form) {
|
||||||
|
return {
|
||||||
|
classificationNumber: form.classificationNumber,
|
||||||
|
categoryCode: form.categoryCode,
|
||||||
|
name: form.name,
|
||||||
|
receiptCode: form.receiptCode,
|
||||||
|
representativeItem: form.representativeItem,
|
||||||
|
requiresDamageWaiver: form.requiresDamageWaiver,
|
||||||
|
isActive: form.isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateGiftCategory(form) {
|
||||||
|
const errors = {}
|
||||||
|
const requiredFields = [
|
||||||
|
['classificationNumber', '분류번호', 3],
|
||||||
|
['categoryCode', '카테고리 코드', 50],
|
||||||
|
['name', '카테고리명', 50],
|
||||||
|
['receiptCode', '접수 코드', 9],
|
||||||
|
['representativeItem', '대표 품목', 100]
|
||||||
|
]
|
||||||
|
for (const [key, label, maxLength] of requiredFields) {
|
||||||
|
const value = form[key]
|
||||||
|
if (typeof value !== 'string' || !value.trim()) {
|
||||||
|
errors[key] = `${label} 항목은 필수입니다.`
|
||||||
|
} else if (value.length > maxLength) {
|
||||||
|
errors[key] = `${label} 항목은 ${maxLength}자 이하로 입력해 주세요.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!errors.classificationNumber && (!/^[0-9]{3}$/.test(form.classificationNumber) || form.classificationNumber.length !== 3)) {
|
||||||
|
errors.classificationNumber = '분류번호는 숫자 3자리로 입력해 주세요.'
|
||||||
|
}
|
||||||
|
if (!errors.receiptCode && /[^A-Z]/.test(form.receiptCode)) {
|
||||||
|
errors.receiptCode = '접수 코드는 영문 대문자 1~9자로 입력해 주세요.'
|
||||||
|
}
|
||||||
|
if (typeof form.requiresDamageWaiver !== 'boolean') {
|
||||||
|
errors.requiresDamageWaiver = '파손면책 동의 필요 여부를 선택해 주세요.'
|
||||||
|
}
|
||||||
|
if (typeof form.isActive !== 'boolean') {
|
||||||
|
errors.isActive = '활성 여부를 선택해 주세요.'
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export const giftSizes = Object.freeze(['SMALL', 'MEDIUM', 'LARGE'])
|
||||||
|
|
||||||
|
export function createGiftSizeForm(item = {}) {
|
||||||
|
return {
|
||||||
|
sizeCode: item.sizeCode || '',
|
||||||
|
name: item.name || '',
|
||||||
|
basePriceCan: item.basePriceCan === undefined ? '' : item.basePriceCan,
|
||||||
|
salePriceCan: item.salePriceCan === undefined ? '' : item.salePriceCan,
|
||||||
|
isActive: item.isActive === undefined ? false : item.isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGiftSizeRequest(form) {
|
||||||
|
return {
|
||||||
|
basePriceCan: Number(form.basePriceCan),
|
||||||
|
salePriceCan: Number(form.salePriceCan),
|
||||||
|
isActive: form.isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateGiftSize(form) {
|
||||||
|
const errors = {}
|
||||||
|
if (!giftSizes.includes(form.sizeCode)) errors.sizeCode = '올바른 선물 사이즈가 아닙니다.'
|
||||||
|
for (const [key, label] of [['basePriceCan', '기본가'], ['salePriceCan', '판매가']]) {
|
||||||
|
const value = form[key]
|
||||||
|
if (value === null || value === undefined || (typeof value === 'string' && !value.trim())) {
|
||||||
|
errors[key] = `${label}를 입력해 주세요.`
|
||||||
|
} else if (!['number', 'string'].includes(typeof value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1) {
|
||||||
|
errors[key] = `${label}는 1 이상의 정수로 입력해 주세요.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!errors.basePriceCan && !errors.salePriceCan && Number(form.salePriceCan) > Number(form.basePriceCan)) {
|
||||||
|
errors.salePriceCan = '판매가는 기본가보다 클 수 없습니다.'
|
||||||
|
}
|
||||||
|
if (typeof form.isActive !== 'boolean') errors.isActive = '활성 여부를 선택해 주세요.'
|
||||||
|
return errors
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<v-btn
|
||||||
|
v-for="action in visibleActions"
|
||||||
|
:key="action.value"
|
||||||
|
:color="action.value === 'MARK_UNDELIVERABLE' ? 'error' : 'primary'"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="ma-1"
|
||||||
|
small
|
||||||
|
outlined
|
||||||
|
@click.stop="$emit('select', action.value)"
|
||||||
|
>
|
||||||
|
{{ action.label }}
|
||||||
|
</v-btn>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { giftActions } from '@/utils/gift'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GiftActions',
|
||||||
|
props: {
|
||||||
|
actions: { type: Array, default: () => [] },
|
||||||
|
disabled: { type: Boolean, default: false }
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visibleActions() {
|
||||||
|
return giftActions.filter(action => this.actions.includes(action.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-toolbar dark>
|
||||||
|
<v-spacer />
|
||||||
|
<v-toolbar-title>선물 카테고리</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
@click="openCreate"
|
||||||
|
>
|
||||||
|
카테고리 등록
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<div class="mb-2">
|
||||||
|
총 {{ categories.length }}건
|
||||||
|
</div>
|
||||||
|
<v-progress-linear
|
||||||
|
v-if="is_loading"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
<v-simple-table class="elevation-10">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="header in headers"
|
||||||
|
:key="header"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
{{ header }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="item in categories"
|
||||||
|
:key="item.categoryId"
|
||||||
|
>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.categoryId }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.classificationNumber }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.categoryCode }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.name }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.receiptCode }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.representativeItem }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.requiresDamageWaiver ? '예' : '아니오' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.isActive ? '활성' : '비활성' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
outlined
|
||||||
|
color="primary"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
@click="openEdit(item)"
|
||||||
|
>
|
||||||
|
수정
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
outlined
|
||||||
|
color="error"
|
||||||
|
class="ma-1"
|
||||||
|
:disabled="!item.isActive || is_loading || is_operating"
|
||||||
|
@click="requestDeactivation(item)"
|
||||||
|
>
|
||||||
|
비활성화
|
||||||
|
</v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!categories.length">
|
||||||
|
<td
|
||||||
|
:colspan="headers.length"
|
||||||
|
class="text-center grey--text"
|
||||||
|
>
|
||||||
|
{{ is_loading ? '조회 중입니다.' : list_error ? '목록을 불러오지 못했습니다.' : '등록된 카테고리가 없습니다.' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-simple-table>
|
||||||
|
<v-btn
|
||||||
|
v-if="list_error"
|
||||||
|
class="mt-3"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
@click="getCategories"
|
||||||
|
>
|
||||||
|
다시 조회
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="show_form_dialog"
|
||||||
|
max-width="560px"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-form @submit.prevent="saveCategory">
|
||||||
|
<v-card-title class="headline">
|
||||||
|
{{ isEdit ? '카테고리 수정' : '카테고리 등록' }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-for="field in fields"
|
||||||
|
:key="field.key"
|
||||||
|
v-model="form[field.key]"
|
||||||
|
:label="field.label"
|
||||||
|
:placeholder="field.placeholder"
|
||||||
|
:counter="field.maxLength"
|
||||||
|
:readonly="isEdit && field.key === 'categoryCode'"
|
||||||
|
:hint="isEdit && field.key === 'categoryCode' ? '카테고리 코드는 수정할 수 없습니다.' : ''"
|
||||||
|
:persistent-hint="isEdit && field.key === 'categoryCode'"
|
||||||
|
:disabled="is_operating"
|
||||||
|
:error-messages="form_errors[field.key]"
|
||||||
|
@input="$set(form_errors, field.key, '')"
|
||||||
|
/>
|
||||||
|
<v-switch
|
||||||
|
v-model="form.requiresDamageWaiver"
|
||||||
|
label="파손면책 동의 필요 여부"
|
||||||
|
:disabled="is_operating"
|
||||||
|
:error-messages="form_errors.requiresDamageWaiver"
|
||||||
|
@change="$set(form_errors, 'requiresDamageWaiver', '')"
|
||||||
|
/>
|
||||||
|
<v-switch
|
||||||
|
v-model="form.isActive"
|
||||||
|
label="활성 여부"
|
||||||
|
:disabled="is_operating"
|
||||||
|
:error-messages="form_errors.isActive"
|
||||||
|
@change="$set(form_errors, 'isActive', '')"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
:disabled="is_operating"
|
||||||
|
@click="closeForm"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="is_operating"
|
||||||
|
:disabled="is_operating"
|
||||||
|
>
|
||||||
|
{{ isEdit ? '수정' : '등록' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="show_confirm_dialog"
|
||||||
|
max-width="500px"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>카테고리 비활성화 확인</v-card-title>
|
||||||
|
<v-card-text v-if="pendingCategory">
|
||||||
|
<p>{{ pendingCategory.name }} 카테고리를 비활성화하시겠습니까?</p>
|
||||||
|
<p>실제 삭제되지 않고 비활성 상태로 변경됩니다.</p>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
:disabled="is_operating"
|
||||||
|
@click="cancelDeactivation"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
:loading="is_operating"
|
||||||
|
:disabled="is_operating"
|
||||||
|
@click="deactivateCategory"
|
||||||
|
>
|
||||||
|
확인
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as api from '@/api/gift_category'
|
||||||
|
import { createGiftCategoryForm, buildGiftCategoryRequest, validateGiftCategory } from '@/utils/gift_category'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GiftCategory',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
headers: ['카테고리 ID', '분류번호', '카테고리 코드', '카테고리명', '접수 코드', '대표 품목', '파손면책 동의 필요 여부', '활성 여부', '관리'],
|
||||||
|
fields: [
|
||||||
|
{ key: 'classificationNumber', label: '분류번호', placeholder: '예: 100', maxLength: 3 },
|
||||||
|
{ key: 'categoryCode', label: '카테고리 코드', placeholder: '예: DOLL', maxLength: 50 },
|
||||||
|
{ key: 'name', label: '카테고리명', placeholder: '예: 인형', maxLength: 50 },
|
||||||
|
{ key: 'receiptCode', label: '접수 코드', placeholder: '예: A', maxLength: 9 },
|
||||||
|
{ key: 'representativeItem', label: '대표 품목', placeholder: '예: 피규어', maxLength: 100 }
|
||||||
|
],
|
||||||
|
categories: [],
|
||||||
|
is_loading: false,
|
||||||
|
list_error: false,
|
||||||
|
listRequestId: 0,
|
||||||
|
is_operating: false,
|
||||||
|
show_form_dialog: false,
|
||||||
|
categoryId: null,
|
||||||
|
form: createGiftCategoryForm(),
|
||||||
|
form_errors: {},
|
||||||
|
show_confirm_dialog: false,
|
||||||
|
pendingCategory: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isEdit() {
|
||||||
|
return this.categoryId !== null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.getCategories()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.listRequestId++
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
notifyError(error) {
|
||||||
|
const response = error.response && error.response.data
|
||||||
|
this.$dialog.notify.error((response && response.message) || error.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||||
|
},
|
||||||
|
checkResponse(response) {
|
||||||
|
if (response.success !== true) throw new Error(response.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||||
|
},
|
||||||
|
async getCategories() {
|
||||||
|
const requestId = ++this.listRequestId
|
||||||
|
this.is_loading = true
|
||||||
|
this.list_error = false
|
||||||
|
try {
|
||||||
|
const response = await api.getGiftCategories()
|
||||||
|
if (requestId !== this.listRequestId) return
|
||||||
|
this.checkResponse(response)
|
||||||
|
this.categories = response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (requestId !== this.listRequestId) return
|
||||||
|
this.categories = []
|
||||||
|
this.list_error = true
|
||||||
|
this.notifyError(error)
|
||||||
|
} finally {
|
||||||
|
if (requestId === this.listRequestId) this.is_loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCreate() {
|
||||||
|
if (this.is_loading || this.is_operating) return
|
||||||
|
this.categoryId = null
|
||||||
|
this.form = createGiftCategoryForm()
|
||||||
|
this.form_errors = {}
|
||||||
|
this.show_form_dialog = true
|
||||||
|
},
|
||||||
|
openEdit(item) {
|
||||||
|
if (this.is_loading || this.is_operating) return
|
||||||
|
this.categoryId = item.categoryId
|
||||||
|
this.form = buildGiftCategoryRequest(item)
|
||||||
|
this.form_errors = {}
|
||||||
|
this.show_form_dialog = true
|
||||||
|
},
|
||||||
|
closeForm() {
|
||||||
|
if (this.is_operating) return
|
||||||
|
this.show_form_dialog = false
|
||||||
|
},
|
||||||
|
async saveCategory() {
|
||||||
|
if (!this.show_form_dialog || this.is_operating) return
|
||||||
|
this.form_errors = validateGiftCategory(this.form)
|
||||||
|
if (Object.keys(this.form_errors).length) return
|
||||||
|
this.is_operating = true
|
||||||
|
try {
|
||||||
|
const response = this.isEdit
|
||||||
|
? await api.updateGiftCategory(this.categoryId, this.form)
|
||||||
|
: await api.createGiftCategory(this.form)
|
||||||
|
this.checkResponse(response)
|
||||||
|
this.$dialog.notify.success(this.isEdit ? '카테고리가 수정되었습니다.' : '카테고리가 등록되었습니다.')
|
||||||
|
await this.getCategories()
|
||||||
|
this.show_form_dialog = false
|
||||||
|
} catch (error) {
|
||||||
|
this.notifyError(error)
|
||||||
|
} finally {
|
||||||
|
this.is_operating = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestDeactivation(item) {
|
||||||
|
if (!item.isActive || this.is_loading || this.is_operating) return
|
||||||
|
this.pendingCategory = item
|
||||||
|
this.show_confirm_dialog = true
|
||||||
|
},
|
||||||
|
cancelDeactivation() {
|
||||||
|
if (this.is_operating) return
|
||||||
|
this.show_confirm_dialog = false
|
||||||
|
this.pendingCategory = null
|
||||||
|
},
|
||||||
|
async deactivateCategory() {
|
||||||
|
if (!this.show_confirm_dialog || !this.pendingCategory || !this.pendingCategory.isActive || this.is_operating) return
|
||||||
|
this.is_operating = true
|
||||||
|
try {
|
||||||
|
const response = await api.deactivateGiftCategory(this.pendingCategory.categoryId)
|
||||||
|
this.checkResponse(response)
|
||||||
|
const index = this.categories.findIndex(item => item.categoryId === this.pendingCategory.categoryId)
|
||||||
|
if (index !== -1) this.$set(this.categories, index, response.data)
|
||||||
|
this.show_confirm_dialog = false
|
||||||
|
this.pendingCategory = null
|
||||||
|
this.$dialog.notify.success('카테고리가 비활성화되었습니다.')
|
||||||
|
} catch (error) {
|
||||||
|
this.notifyError(error)
|
||||||
|
} finally {
|
||||||
|
this.is_operating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,562 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-toolbar dark>
|
||||||
|
<v-spacer />
|
||||||
|
<v-toolbar-title>선물함 리스트</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<v-container fluid>
|
||||||
|
<v-form @submit.prevent="search">
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="2"
|
||||||
|
>
|
||||||
|
<v-select
|
||||||
|
v-model="filters.status"
|
||||||
|
:items="giftStatuses"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
label="상태"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="3"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="filters.applicationNo"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
label="신청번호"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="filters.nickname"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
label="발송인 또는 수취인 닉네임"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="3"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="#3bb9f1"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
검색
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
class="ml-2"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
@click="reset"
|
||||||
|
>
|
||||||
|
초기화
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<div class="mb-2">
|
||||||
|
총 {{ totalCount }}건
|
||||||
|
</div>
|
||||||
|
<v-progress-linear
|
||||||
|
v-if="is_loading"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
<v-simple-table class="elevation-10">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="header in headers"
|
||||||
|
:key="header"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
{{ header }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="item in gifts"
|
||||||
|
:key="item.applicationNo"
|
||||||
|
>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.applicationNo }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.senderNickname }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.recipientNickname }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.sizeName }} / {{ item.sizeCode }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.categoryName }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.classificationNumber }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.courierCompanyName || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.trackingNumber || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.statusName }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-btn
|
||||||
|
class="ma-1"
|
||||||
|
small
|
||||||
|
:disabled="is_operating || is_loading"
|
||||||
|
@click="openDetail(item.applicationNo)"
|
||||||
|
>
|
||||||
|
상세
|
||||||
|
</v-btn>
|
||||||
|
<gift-actions
|
||||||
|
:actions="item.availableActions"
|
||||||
|
:disabled="is_operating || is_loading"
|
||||||
|
@select="requestAction(item, $event)"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!gifts.length">
|
||||||
|
<td
|
||||||
|
:colspan="headers.length"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
{{ is_loading ? '조회 중입니다.' : list_error ? '목록을 불러오지 못했습니다. 다시 검색해 주세요.' : '조회된 선물 신청이 없습니다.' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-simple-table>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row class="text-center">
|
||||||
|
<v-col>
|
||||||
|
<v-pagination
|
||||||
|
:value="page"
|
||||||
|
:length="total_page"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
:total-visible="10"
|
||||||
|
circle
|
||||||
|
@input="changePage"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="show_detail_dialog"
|
||||||
|
max-width="1000px"
|
||||||
|
persistent
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>선물 상세</v-card-title>
|
||||||
|
<v-progress-linear
|
||||||
|
v-if="is_detail_loading"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
<v-card-text>
|
||||||
|
<template v-if="detail">
|
||||||
|
<div class="mb-4">
|
||||||
|
신청번호: {{ detail.applicationNo }} / 현재 상태: {{ detail.statusName }}
|
||||||
|
</div>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="member in memberSections"
|
||||||
|
:key="member.key"
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<h3 class="mb-2">
|
||||||
|
{{ member.title }}
|
||||||
|
</h3>
|
||||||
|
<v-simple-table>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="field in memberFields"
|
||||||
|
:key="field.key"
|
||||||
|
>
|
||||||
|
<th>{{ field.label }}</th>
|
||||||
|
<td>{{ detail[member.key][field.key] || (member.key === 'recipientInfo' && field.key !== 'nickname' ? '미입력' : '-') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-simple-table>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<h3 class="mb-2">
|
||||||
|
상품 정보
|
||||||
|
</h3>
|
||||||
|
<v-simple-table>
|
||||||
|
<tbody>
|
||||||
|
<tr><th>사이즈</th><td>{{ detail.productInfo.sizeName }} / {{ detail.productInfo.sizeCode }}</td></tr>
|
||||||
|
<tr><th>카테고리</th><td>{{ detail.productInfo.categoryName }}</td></tr>
|
||||||
|
<tr><th>분류번호</th><td>{{ detail.productInfo.classificationNumber }}</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</v-simple-table>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<h3 class="mb-2">
|
||||||
|
입고 배송 정보
|
||||||
|
</h3>
|
||||||
|
<v-simple-table>
|
||||||
|
<tbody>
|
||||||
|
<tr><th>택배사</th><td>{{ detail.inboundDeliveryInfo.courierCompanyName || '-' }}</td></tr>
|
||||||
|
<tr><th>운송장번호</th><td>{{ detail.inboundDeliveryInfo.trackingNumber || '-' }}</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</v-simple-table>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="mb-2">
|
||||||
|
가능한 액션
|
||||||
|
</h3>
|
||||||
|
<gift-actions
|
||||||
|
:actions="detail.availableActions"
|
||||||
|
:disabled="is_operating || is_detail_loading || is_loading"
|
||||||
|
@select="requestAction(detail, $event)"
|
||||||
|
/>
|
||||||
|
<span v-if="!detail.availableActions.length">현재 가능한 액션이 없습니다.</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else-if="!is_detail_loading">
|
||||||
|
상세 정보를 불러오지 못했습니다.
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
color="primary"
|
||||||
|
@click="getDetail(detailApplicationNo)"
|
||||||
|
>
|
||||||
|
다시 시도
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
:disabled="is_operating"
|
||||||
|
@click="closeDetail"
|
||||||
|
>
|
||||||
|
닫기
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="show_reason_dialog"
|
||||||
|
max-width="500px"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>전달 불가 처리</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p>신청번호: {{ pendingAction ? pendingAction.applicationNo : '' }}</p>
|
||||||
|
<v-form @submit.prevent="confirmReason">
|
||||||
|
<v-textarea
|
||||||
|
v-model="reason"
|
||||||
|
label="전달 불가 사유"
|
||||||
|
:rules="[validateUndeliverableReason]"
|
||||||
|
:error-messages="reason_error"
|
||||||
|
:disabled="is_operating"
|
||||||
|
counter="255"
|
||||||
|
maxlength="255"
|
||||||
|
auto-grow
|
||||||
|
rows="3"
|
||||||
|
@input="reason_error = ''"
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
:disabled="is_operating"
|
||||||
|
@click="cancelAction"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
:disabled="is_operating"
|
||||||
|
@click="confirmReason"
|
||||||
|
>
|
||||||
|
확인
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="show_confirm_dialog"
|
||||||
|
max-width="400px"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>상태 변경 확인</v-card-title>
|
||||||
|
<v-card-text v-if="pendingAction">
|
||||||
|
<p>신청번호: {{ pendingAction.applicationNo }}</p>
|
||||||
|
<p>{{ pendingAction.label }}하시겠습니까?</p>
|
||||||
|
<p
|
||||||
|
v-if="pendingAction.value === 'MARK_UNDELIVERABLE'"
|
||||||
|
class="gift-reason"
|
||||||
|
>
|
||||||
|
사유: {{ reason.trim() }}
|
||||||
|
</p>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
:disabled="is_operating"
|
||||||
|
@click="cancelConfirmation"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
:loading="is_operating"
|
||||||
|
:disabled="is_operating"
|
||||||
|
@click="executeAction"
|
||||||
|
>
|
||||||
|
확인
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as api from '@/api/gift'
|
||||||
|
import { giftActions, giftStatuses, validateUndeliverableReason } from '@/utils/gift'
|
||||||
|
import GiftActions from './GiftActions.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GiftList',
|
||||||
|
components: { GiftActions },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
giftStatuses,
|
||||||
|
headers: ['신청번호', '발송인 닉네임', '수취인 닉네임', '사이즈', '카테고리', '분류번호', '택배사', '운송장번호', '상태', '액션'],
|
||||||
|
memberSections: [{ key: 'senderInfo', title: '발송인 정보' }, { key: 'recipientInfo', title: '수취인 정보' }],
|
||||||
|
memberFields: [{ key: 'nickname', label: '닉네임' }, { key: 'name', label: '이름' }, { key: 'phoneNumber', label: '전화번호' }, { key: 'address', label: '주소' }],
|
||||||
|
filters: { status: '', applicationNo: '', nickname: '' },
|
||||||
|
appliedFilters: { status: '', applicationNo: '', nickname: '' },
|
||||||
|
gifts: [],
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
totalCount: 0,
|
||||||
|
hasNext: false,
|
||||||
|
is_loading: false,
|
||||||
|
list_error: false,
|
||||||
|
listRequestId: 0,
|
||||||
|
detail: null,
|
||||||
|
detailApplicationNo: '',
|
||||||
|
detailRequestId: 0,
|
||||||
|
is_detail_loading: false,
|
||||||
|
show_detail_dialog: false,
|
||||||
|
is_operating: false,
|
||||||
|
pendingAction: null,
|
||||||
|
show_reason_dialog: false,
|
||||||
|
show_confirm_dialog: false,
|
||||||
|
reason: '',
|
||||||
|
reason_error: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
total_page() {
|
||||||
|
const count = Math.max(1, Math.ceil(this.totalCount / this.size))
|
||||||
|
return this.hasNext ? Math.max(count, this.page + 1) : Math.max(1, this.page)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.getGifts()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.listRequestId++
|
||||||
|
this.detailRequestId++
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
validateUndeliverableReason,
|
||||||
|
notifyError(error) {
|
||||||
|
const response = error.response && error.response.data
|
||||||
|
this.$dialog.notify.error((response && response.message) || error.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||||
|
},
|
||||||
|
checkResponse(response) {
|
||||||
|
if (response.success !== true) throw new Error(response.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||||
|
},
|
||||||
|
async getGifts(page = this.page) {
|
||||||
|
const requestId = ++this.listRequestId
|
||||||
|
this.is_loading = true
|
||||||
|
this.list_error = false
|
||||||
|
try {
|
||||||
|
const response = await api.getGifts({ ...this.appliedFilters, page: page - 1, size: this.size })
|
||||||
|
if (requestId !== this.listRequestId) return
|
||||||
|
this.checkResponse(response)
|
||||||
|
this.gifts = response.data.items
|
||||||
|
this.page = response.data.page + 1
|
||||||
|
this.size = response.data.size
|
||||||
|
this.totalCount = response.data.totalCount
|
||||||
|
this.hasNext = response.data.hasNext
|
||||||
|
} catch (error) {
|
||||||
|
if (requestId !== this.listRequestId) return
|
||||||
|
this.gifts = []
|
||||||
|
this.totalCount = 0
|
||||||
|
this.hasNext = false
|
||||||
|
this.list_error = true
|
||||||
|
this.notifyError(error)
|
||||||
|
} finally {
|
||||||
|
if (requestId === this.listRequestId) this.is_loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async search() {
|
||||||
|
if (this.is_loading || this.is_operating) return
|
||||||
|
this.appliedFilters = {
|
||||||
|
status: this.filters.status || '',
|
||||||
|
applicationNo: (this.filters.applicationNo || '').trim(),
|
||||||
|
nickname: (this.filters.nickname || '').trim()
|
||||||
|
}
|
||||||
|
await this.getGifts(1)
|
||||||
|
},
|
||||||
|
async reset() {
|
||||||
|
if (this.is_loading || this.is_operating) return
|
||||||
|
this.filters = { status: '', applicationNo: '', nickname: '' }
|
||||||
|
await this.search()
|
||||||
|
},
|
||||||
|
async changePage(page) {
|
||||||
|
if (this.is_loading || this.is_operating) return
|
||||||
|
await this.getGifts(page)
|
||||||
|
},
|
||||||
|
async openDetail(applicationNo) {
|
||||||
|
if (this.is_operating) return
|
||||||
|
this.detail = null
|
||||||
|
this.show_detail_dialog = true
|
||||||
|
await this.getDetail(applicationNo)
|
||||||
|
},
|
||||||
|
async getDetail(applicationNo) {
|
||||||
|
const requestId = ++this.detailRequestId
|
||||||
|
this.detailApplicationNo = applicationNo
|
||||||
|
this.is_detail_loading = true
|
||||||
|
try {
|
||||||
|
const response = await api.getGift(applicationNo)
|
||||||
|
if (requestId !== this.detailRequestId) return
|
||||||
|
this.checkResponse(response)
|
||||||
|
this.detail = response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (requestId !== this.detailRequestId) return
|
||||||
|
this.detail = null
|
||||||
|
this.notifyError(error)
|
||||||
|
} finally {
|
||||||
|
if (requestId === this.detailRequestId) this.is_detail_loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeDetail() {
|
||||||
|
if (this.is_operating) return
|
||||||
|
this.detailRequestId++
|
||||||
|
this.show_detail_dialog = false
|
||||||
|
this.is_detail_loading = false
|
||||||
|
this.detail = null
|
||||||
|
this.detailApplicationNo = ''
|
||||||
|
},
|
||||||
|
requestAction(item, value) {
|
||||||
|
if (this.is_operating || this.is_loading || this.is_detail_loading || this.pendingAction) return
|
||||||
|
const action = giftActions.find(action => action.value === value)
|
||||||
|
if (!action || !item.availableActions.includes(value)) return
|
||||||
|
this.pendingAction = { ...action, applicationNo: item.applicationNo }
|
||||||
|
this.reason = ''
|
||||||
|
this.reason_error = ''
|
||||||
|
if (value === 'MARK_UNDELIVERABLE') {
|
||||||
|
this.show_reason_dialog = true
|
||||||
|
} else {
|
||||||
|
this.show_confirm_dialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmReason() {
|
||||||
|
if (this.is_operating || !this.pendingAction || this.pendingAction.value !== 'MARK_UNDELIVERABLE') return
|
||||||
|
const validation = validateUndeliverableReason(this.reason)
|
||||||
|
this.reason_error = validation === true ? '' : validation
|
||||||
|
if (validation === true) this.show_confirm_dialog = true
|
||||||
|
},
|
||||||
|
cancelConfirmation() {
|
||||||
|
if (this.is_operating) return
|
||||||
|
if (this.pendingAction && this.pendingAction.value === 'MARK_UNDELIVERABLE') {
|
||||||
|
this.show_confirm_dialog = false
|
||||||
|
} else {
|
||||||
|
this.cancelAction()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancelAction() {
|
||||||
|
if (this.is_operating) return
|
||||||
|
this.show_confirm_dialog = false
|
||||||
|
this.show_reason_dialog = false
|
||||||
|
this.pendingAction = null
|
||||||
|
this.reason = ''
|
||||||
|
this.reason_error = ''
|
||||||
|
},
|
||||||
|
async executeAction() {
|
||||||
|
if (this.is_operating || !this.pendingAction || !this.show_confirm_dialog) return
|
||||||
|
const action = this.pendingAction
|
||||||
|
if (action.value === 'MARK_UNDELIVERABLE' && validateUndeliverableReason(this.reason) !== true) {
|
||||||
|
this.show_confirm_dialog = false
|
||||||
|
this.confirmReason()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.is_operating = true
|
||||||
|
try {
|
||||||
|
const response = action.value === 'MARK_UNDELIVERABLE'
|
||||||
|
? await api.markUndeliverable(action.applicationNo, this.reason.trim())
|
||||||
|
: await api[action.method](action.applicationNo)
|
||||||
|
this.checkResponse(response)
|
||||||
|
this.show_confirm_dialog = false
|
||||||
|
this.show_reason_dialog = false
|
||||||
|
this.pendingAction = null
|
||||||
|
this.reason = ''
|
||||||
|
this.$dialog.notify.success(`${action.label}되었습니다.`)
|
||||||
|
const refreshes = [this.getGifts()]
|
||||||
|
if (this.show_detail_dialog) refreshes.push(this.getDetail(this.detail ? this.detail.applicationNo : this.detailApplicationNo))
|
||||||
|
await Promise.all(refreshes)
|
||||||
|
} catch (error) {
|
||||||
|
this.notifyError(error)
|
||||||
|
} finally {
|
||||||
|
this.is_operating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gift-reason {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-toolbar dark>
|
||||||
|
<v-spacer />
|
||||||
|
<v-toolbar-title>선물 사이즈</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
v-if="availableSizes.length"
|
||||||
|
color="primary"
|
||||||
|
:disabled="!canCreate"
|
||||||
|
@click="openCreate"
|
||||||
|
>
|
||||||
|
사이즈 등록
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<div class="mb-2">
|
||||||
|
총 {{ sizes.length }}건
|
||||||
|
</div>
|
||||||
|
<v-progress-linear
|
||||||
|
v-if="is_loading"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
<v-simple-table class="elevation-10">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="header in headers"
|
||||||
|
:key="header"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
{{ header }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="item in sizes"
|
||||||
|
:key="item.sizeCode"
|
||||||
|
>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.sizeCode }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.name }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.basePriceCan.toLocaleString() }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.salePriceCan.toLocaleString() }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{{ item.isActive ? '활성' : '비활성' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
outlined
|
||||||
|
color="primary"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
@click="openEdit(item)"
|
||||||
|
>
|
||||||
|
수정
|
||||||
|
</v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!sizes.length">
|
||||||
|
<td
|
||||||
|
:colspan="headers.length"
|
||||||
|
class="text-center grey--text"
|
||||||
|
>
|
||||||
|
{{ is_loading ? '조회 중입니다.' : list_error ? '목록을 불러오지 못했습니다.' : '등록된 선물 사이즈가 없습니다.' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-simple-table>
|
||||||
|
<v-btn
|
||||||
|
v-if="list_error"
|
||||||
|
class="mt-3"
|
||||||
|
:disabled="is_loading || is_operating"
|
||||||
|
@click="getSizes"
|
||||||
|
>
|
||||||
|
다시 조회
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="show_form_dialog"
|
||||||
|
max-width="560px"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-form @submit.prevent="saveSize">
|
||||||
|
<v-card-title class="headline">
|
||||||
|
선물 사이즈 {{ isEdit ? '수정' : '등록' }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-if="isEdit"
|
||||||
|
:value="form.sizeCode"
|
||||||
|
label="사이즈 코드"
|
||||||
|
readonly
|
||||||
|
:error-messages="form_errors.sizeCode"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-else
|
||||||
|
v-model="form.sizeCode"
|
||||||
|
:items="availableSizes"
|
||||||
|
label="사이즈 코드"
|
||||||
|
hint="미등록 사이즈만 등록할 수 있습니다."
|
||||||
|
persistent-hint
|
||||||
|
:disabled="is_operating"
|
||||||
|
:error-messages="form_errors.sizeCode"
|
||||||
|
@change="$set(form_errors, 'sizeCode', '')"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-if="isEdit"
|
||||||
|
:value="form.name"
|
||||||
|
label="사이즈명"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.basePriceCan"
|
||||||
|
label="기본가"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
:disabled="is_operating"
|
||||||
|
:error-messages="form_errors.basePriceCan"
|
||||||
|
@input="clearPriceErrors"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.salePriceCan"
|
||||||
|
label="판매가"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
:disabled="is_operating"
|
||||||
|
:error-messages="form_errors.salePriceCan"
|
||||||
|
@input="clearPriceErrors"
|
||||||
|
/>
|
||||||
|
<v-switch
|
||||||
|
v-model="form.isActive"
|
||||||
|
label="활성 여부"
|
||||||
|
:disabled="is_operating"
|
||||||
|
:error-messages="form_errors.isActive"
|
||||||
|
@change="$set(form_errors, 'isActive', '')"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="blue darken-1"
|
||||||
|
text
|
||||||
|
:disabled="is_operating"
|
||||||
|
@click="closeForm"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="is_operating"
|
||||||
|
:disabled="is_operating || is_loading || (!isEdit && (!list_loaded || list_error))"
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as api from '@/api/gift_size'
|
||||||
|
import { giftSizes, createGiftSizeForm, buildGiftSizeRequest, validateGiftSize } from '@/utils/gift_size'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GiftSize',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
headers: ['사이즈 코드', '사이즈명', '기본가', '판매가', '활성 여부', '관리'],
|
||||||
|
sizes: [],
|
||||||
|
is_loading: false,
|
||||||
|
list_loaded: false,
|
||||||
|
list_error: false,
|
||||||
|
listRequestId: 0,
|
||||||
|
is_operating: false,
|
||||||
|
show_form_dialog: false,
|
||||||
|
isEdit: false,
|
||||||
|
form: createGiftSizeForm(),
|
||||||
|
form_errors: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
availableSizes() {
|
||||||
|
return giftSizes.filter(sizeCode => !this.sizes.some(item => item.sizeCode === sizeCode))
|
||||||
|
},
|
||||||
|
canCreate() {
|
||||||
|
return this.list_loaded && !this.list_error && !this.is_loading && !this.is_operating && this.availableSizes.length > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.getSizes()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.listRequestId++
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
notifyError(error) {
|
||||||
|
const response = error.response && error.response.data
|
||||||
|
this.$dialog.notify.error((response && response.message) || error.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||||
|
},
|
||||||
|
checkResponse(response) {
|
||||||
|
if (response.success !== true) throw new Error(response.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
||||||
|
},
|
||||||
|
async getSizes() {
|
||||||
|
const requestId = ++this.listRequestId
|
||||||
|
this.is_loading = true
|
||||||
|
this.list_loaded = false
|
||||||
|
this.list_error = false
|
||||||
|
try {
|
||||||
|
const response = await api.getGiftSizePrices()
|
||||||
|
if (requestId !== this.listRequestId) return
|
||||||
|
this.checkResponse(response)
|
||||||
|
this.sizes = response.data
|
||||||
|
this.list_loaded = true
|
||||||
|
} catch (error) {
|
||||||
|
if (requestId !== this.listRequestId) return
|
||||||
|
this.sizes = []
|
||||||
|
this.list_error = true
|
||||||
|
this.notifyError(error)
|
||||||
|
} finally {
|
||||||
|
if (requestId === this.listRequestId) this.is_loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCreate() {
|
||||||
|
if (!this.canCreate) return
|
||||||
|
this.isEdit = false
|
||||||
|
this.form = createGiftSizeForm()
|
||||||
|
this.form_errors = {}
|
||||||
|
this.show_form_dialog = true
|
||||||
|
},
|
||||||
|
openEdit(item) {
|
||||||
|
if (this.is_loading || this.is_operating) return
|
||||||
|
this.isEdit = true
|
||||||
|
this.form = createGiftSizeForm(item)
|
||||||
|
this.form_errors = {}
|
||||||
|
this.show_form_dialog = true
|
||||||
|
},
|
||||||
|
closeForm() {
|
||||||
|
if (this.is_operating) return
|
||||||
|
this.show_form_dialog = false
|
||||||
|
},
|
||||||
|
clearPriceErrors() {
|
||||||
|
this.$set(this.form_errors, 'basePriceCan', '')
|
||||||
|
this.$set(this.form_errors, 'salePriceCan', '')
|
||||||
|
},
|
||||||
|
async saveSize() {
|
||||||
|
if (!this.show_form_dialog || this.is_operating || this.is_loading) return
|
||||||
|
if (!this.isEdit && (!this.list_loaded || this.list_error)) return
|
||||||
|
this.form_errors = validateGiftSize(this.form)
|
||||||
|
if (!this.isEdit && !this.form_errors.sizeCode && !this.availableSizes.includes(this.form.sizeCode)) {
|
||||||
|
this.$set(this.form_errors, 'sizeCode', '이미 등록된 사이즈입니다. 수정 버튼을 이용해 주세요.')
|
||||||
|
}
|
||||||
|
if (Object.keys(this.form_errors).length) return
|
||||||
|
const sizeCode = this.form.sizeCode
|
||||||
|
const request = buildGiftSizeRequest(this.form)
|
||||||
|
const action = this.isEdit ? '수정' : '등록'
|
||||||
|
this.is_operating = true
|
||||||
|
try {
|
||||||
|
const confirm = await this.$dialog.confirm({
|
||||||
|
title: `선물 사이즈 ${action} 확인`,
|
||||||
|
text: `${this.form.name || sizeCode} 사이즈의 가격과 활성 여부를 ${action}하시겠습니까?`,
|
||||||
|
actions: { false: '취소', true: '저장' }
|
||||||
|
})
|
||||||
|
if (!confirm) return
|
||||||
|
if (!this.isEdit && (!this.list_loaded || this.list_error || !this.availableSizes.includes(sizeCode))) {
|
||||||
|
this.$set(this.form_errors, 'sizeCode', '등록 가능한 사이즈를 다시 확인해 주세요.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const response = await api.updateGiftSizePrice(sizeCode, request)
|
||||||
|
this.checkResponse(response)
|
||||||
|
this.$dialog.notify.success(`선물 사이즈가 ${action}되었습니다.`)
|
||||||
|
await this.getSizes()
|
||||||
|
this.show_form_dialog = false
|
||||||
|
} catch (error) {
|
||||||
|
this.notifyError(error)
|
||||||
|
} finally {
|
||||||
|
this.is_operating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
const assert = require('assert').strict
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const Module = require('module')
|
||||||
|
const { transformSync } = require('@babel/core')
|
||||||
|
const compiler = require('vue-template-compiler')
|
||||||
|
const Vue = require('vue')
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..')
|
||||||
|
const originalResolve = Module._resolveFilename
|
||||||
|
Module._resolveFilename = function(request, parent, ...rest) {
|
||||||
|
return originalResolve.call(this, request.startsWith('@/') ? path.join(root, 'src', request.slice(2)) : request, parent, ...rest)
|
||||||
|
}
|
||||||
|
const originalJs = require.extensions['.js']
|
||||||
|
function compileScript(source, filename) {
|
||||||
|
return transformSync(source, {
|
||||||
|
filename,
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
plugins: ['@babel/plugin-transform-modules-commonjs']
|
||||||
|
}).code
|
||||||
|
}
|
||||||
|
require.extensions['.js'] = (module, filename) => {
|
||||||
|
if (!filename.startsWith(path.join(root, 'src') + path.sep)) return originalJs(module, filename)
|
||||||
|
module._compile(compileScript(fs.readFileSync(filename, 'utf8'), filename), filename)
|
||||||
|
}
|
||||||
|
require.extensions['.vue'] = (module, filename) => {
|
||||||
|
const { script, template } = compiler.parseComponent(fs.readFileSync(filename, 'utf8'))
|
||||||
|
const compiled = compiler.compile(template.content)
|
||||||
|
assert.deepEqual(compiled.errors, [], `${filename}: 템플릿 컴파일`)
|
||||||
|
module._compile(compileScript(script.content, filename), filename)
|
||||||
|
Object.assign(module.exports.default, compiler.compileToFunctions(template.content))
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = require('../src/api/gift')
|
||||||
|
const { giftActions, giftStatuses, validateUndeliverableReason } = require('../src/utils/gift')
|
||||||
|
const GiftActions = require('../src/views/Gift/GiftActions.vue').default
|
||||||
|
const GiftList = require('../src/views/Gift/GiftList.vue').default
|
||||||
|
const SideMenu = require('../src/components/SideMenu.vue').default
|
||||||
|
const tests = []
|
||||||
|
const test = (name, run) => tests.push({ name, run })
|
||||||
|
const envelope = data => ({ success: true, data })
|
||||||
|
const list = (overrides = {}) => ({ items: [], page: 0, size: 20, totalCount: 0, hasNext: false, ...overrides })
|
||||||
|
const detail = (overrides = {}) => ({
|
||||||
|
applicationNo: 'G-001', status: 'TRACKING_REGISTERED', statusName: '발송 확인',
|
||||||
|
availableActions: ['ARRIVE_MAILBOX', 'MARK_UNDELIVERABLE'],
|
||||||
|
senderInfo: { nickname: '발송인', name: '이름', phoneNumber: '010', address: '주소' },
|
||||||
|
recipientInfo: { nickname: '수취인', name: '', phoneNumber: '', address: '' },
|
||||||
|
productInfo: { sizeName: '소형', sizeCode: 'SMALL', categoryName: '문구', classificationNumber: 'C1' },
|
||||||
|
inboundDeliveryInfo: { courierCompanyName: null, trackingNumber: null },
|
||||||
|
...overrides
|
||||||
|
})
|
||||||
|
function page() {
|
||||||
|
const errors = []
|
||||||
|
const successes = []
|
||||||
|
const vm = new Vue({ ...GiftList, created: undefined })
|
||||||
|
vm.$dialog = { notify: { error: message => errors.push(message), success: message => successes.push(message) } }
|
||||||
|
return { vm, errors, successes }
|
||||||
|
}
|
||||||
|
function deferred() {
|
||||||
|
let resolve
|
||||||
|
const promise = new Promise(done => { resolve = done })
|
||||||
|
return { promise, resolve }
|
||||||
|
}
|
||||||
|
function nodes(vnode) {
|
||||||
|
if (!vnode) return []
|
||||||
|
return [vnode].concat((vnode.children || (vnode.componentOptions && vnode.componentOptions.children) || []).flatMap(nodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
test('API: 기본 페이지와 조합 필터를 query로 전달하고 envelope를 반환한다', async () => {
|
||||||
|
const calls = []
|
||||||
|
const response = envelope(list())
|
||||||
|
Vue.axios = { get: async (...args) => { calls.push(args); return { data: response } } }
|
||||||
|
assert.equal(await api.getGifts(), response)
|
||||||
|
assert.deepEqual(calls[0], ['/api/v2/admin/gifts', { params: { page: 0, size: 20 } }])
|
||||||
|
await api.getGifts({ status: 'ARRIVED_AT_MAILBOX', applicationNo: 'G-01', nickname: '닉 네임', page: 2, size: 10 })
|
||||||
|
assert.deepEqual(calls[1][1].params, { status: 'ARRIVED_AT_MAILBOX', applicationNo: 'G-01', nickname: '닉 네임', page: 2, size: 10 })
|
||||||
|
await api.getGifts({ status: '', applicationNo: '', nickname: '' })
|
||||||
|
assert.deepEqual(calls[2][1].params, { page: 0, size: 20 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('API: 상세 및 네 가지 POST 경로/본문을 정확하게 전달한다', async () => {
|
||||||
|
const calls = []
|
||||||
|
Vue.axios = {
|
||||||
|
get: async (...args) => { calls.push(['GET', ...args]); return { data: envelope(detail()) } },
|
||||||
|
post: async (...args) => { calls.push(['POST', ...args]); return { data: envelope({ status: 'ARRIVED_AT_MAILBOX' }) } }
|
||||||
|
}
|
||||||
|
await api.getGift('G/001')
|
||||||
|
await api.arriveMailbox('G/001')
|
||||||
|
await api.completeInspection('G/001')
|
||||||
|
await api.completeDelivery('G/001')
|
||||||
|
await api.markUndeliverable('G/001', '파손')
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['GET', '/api/v2/admin/gifts/G%2F001'],
|
||||||
|
['POST', '/api/v2/admin/gifts/G%2F001/arrive-mailbox'],
|
||||||
|
['POST', '/api/v2/admin/gifts/G%2F001/complete-inspection'],
|
||||||
|
['POST', '/api/v2/admin/gifts/G%2F001/complete-delivery'],
|
||||||
|
['POST', '/api/v2/admin/gifts/G%2F001/mark-undeliverable', { reason: '파손' }]
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('액션: 서버 availableActions에 있는 버튼만 실제 템플릿에서 노출한다', () => {
|
||||||
|
assert.deepEqual(giftActions.map(action => [action.value, action.label]), [
|
||||||
|
['ARRIVE_MAILBOX', '사서함 도착 처리'], ['COMPLETE_INSPECTION', '검수 완료 처리'],
|
||||||
|
['COMPLETE_DELIVERY', '전달 완료 처리'], ['MARK_UNDELIVERABLE', '전달 불가 처리']
|
||||||
|
])
|
||||||
|
const scenarios = [[], ['ARRIVE_MAILBOX', 'MARK_UNDELIVERABLE'], ['COMPLETE_INSPECTION', 'MARK_UNDELIVERABLE'], ['COMPLETE_DELIVERY', 'MARK_UNDELIVERABLE'], [], [], []]
|
||||||
|
for (const actions of scenarios.concat([['UNKNOWN'], ['MARK_UNDELIVERABLE']])) {
|
||||||
|
const vm = new Vue({ ...GiftActions, propsData: { actions } })
|
||||||
|
const buttons = nodes(vm._render()).filter(node => node.tag === 'v-btn')
|
||||||
|
assert.deepEqual(buttons.map(button => nodes(button).map(node => node.text || '').join('').trim()), giftActions.filter(action => actions.includes(action.value)).map(action => action.label))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('필터: 전체와 일곱 가지 상태를 한국어로 제공한다', () => {
|
||||||
|
assert.deepEqual(giftStatuses.map(status => [status.value, status.text]), [
|
||||||
|
['', '전체'], ['RECEIVED', '접수 완료'], ['TRACKING_REGISTERED', '발송 확인'],
|
||||||
|
['ARRIVED_AT_MAILBOX', '사서함 도착'], ['INSPECTION_COMPLETED', '검수 완료'],
|
||||||
|
['DELIVERED', '전달 완료'], ['UNDELIVERABLE', '전달 불가'], ['CANCELED', '신청 취소']
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('액션 버튼: 선택 이벤트와 중복 실행 방지 속성을 연결한다', () => {
|
||||||
|
const vm = new Vue({ ...GiftActions, propsData: { actions: ['ARRIVE_MAILBOX'], disabled: true } })
|
||||||
|
const button = nodes(vm._render()).find(node => node.tag === 'v-btn')
|
||||||
|
assert.equal(button.data.attrs.disabled, true)
|
||||||
|
let selected
|
||||||
|
let stopped = false
|
||||||
|
vm.$on('select', action => { selected = action })
|
||||||
|
button.data.on.click({ stopPropagation: () => { stopped = true } })
|
||||||
|
assert.equal(selected, 'ARRIVE_MAILBOX')
|
||||||
|
assert.equal(stopped, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('전달 불가 사유: 빈 값/공백/256자는 거부하고 255자는 허용한다', () => {
|
||||||
|
for (const value of ['', ' \n\t ', null, undefined, '가'.repeat(256)]) {
|
||||||
|
assert.notEqual(validateUndeliverableReason(value), true)
|
||||||
|
}
|
||||||
|
assert.equal(validateUndeliverableReason('가'.repeat(255)), true)
|
||||||
|
assert.equal(validateUndeliverableReason(' 파손 '), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('목록: 검색은 첫 페이지, 페이지 이동은 적용된 필터와 서버 size를 유지한다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const calls = []
|
||||||
|
Vue.axios = { get: async (url, config) => {
|
||||||
|
calls.push(config.params)
|
||||||
|
return { data: envelope(list({ page: config.params.page, size: 10, totalCount: 25, hasNext: config.params.page < 2 })) }
|
||||||
|
} }
|
||||||
|
vm.filters = { status: 'RECEIVED', applicationNo: ' G-01 ', nickname: ' 닉네임 ' }
|
||||||
|
vm.page = 3
|
||||||
|
await vm.search()
|
||||||
|
assert.deepEqual(calls[0], { status: 'RECEIVED', applicationNo: 'G-01', nickname: '닉네임', page: 0, size: 20 })
|
||||||
|
assert.equal(vm.page, 1)
|
||||||
|
assert.equal(vm.total_page, 3)
|
||||||
|
vm.filters.nickname = '아직 검색하지 않은 값'
|
||||||
|
await vm.changePage(2)
|
||||||
|
assert.deepEqual(calls[1], { status: 'RECEIVED', applicationNo: 'G-01', nickname: '닉네임', page: 1, size: 10 })
|
||||||
|
await vm.reset()
|
||||||
|
assert.deepEqual(calls[2], { page: 0, size: 10 })
|
||||||
|
assert.deepEqual(vm.filters, { status: '', applicationNo: '', nickname: '' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('페이지네이션: 빈 결과 및 마지막 페이지의 hasNext를 반영한다', () => {
|
||||||
|
const { vm } = page()
|
||||||
|
assert.equal(vm.total_page, 1)
|
||||||
|
vm.totalCount = 100
|
||||||
|
vm.page = 2
|
||||||
|
vm.hasNext = false
|
||||||
|
assert.equal(vm.total_page, 2)
|
||||||
|
vm.hasNext = true
|
||||||
|
assert.equal(vm.total_page, 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('목록: 열과 빈 값을 표시하고 상세 버튼과 액션 이벤트를 연결한다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const item = {
|
||||||
|
applicationNo: 'G-001', senderNickname: '발송인', recipientNickname: '수취인',
|
||||||
|
sizeName: '소형', sizeCode: 'S', categoryName: '문구', classificationNumber: 'C1',
|
||||||
|
courierCompanyName: null, trackingNumber: null, statusName: '발송 확인', availableActions: ['ARRIVE_MAILBOX']
|
||||||
|
}
|
||||||
|
vm.gifts = [item]
|
||||||
|
const rendered = nodes(vm._render())
|
||||||
|
const cells = rendered.filter(node => node.tag === 'td').map(node => nodes(node).map(child => child.text || '').join('').trim())
|
||||||
|
assert.deepEqual(cells.slice(0, 9), ['G-001', '발송인', '수취인', '소형 / S', '문구', 'C1', '-', '-', '발송 확인'])
|
||||||
|
assert.equal(rendered.filter(node => node.tag === 'th').length, 10)
|
||||||
|
const actions = rendered.find(node => node.componentOptions && node.componentOptions.Ctor.options.name === 'GiftActions')
|
||||||
|
assert.deepEqual(Array.from(actions.componentOptions.propsData.actions), ['ARRIVE_MAILBOX'])
|
||||||
|
actions.componentOptions.listeners.select('ARRIVE_MAILBOX')
|
||||||
|
assert.equal(vm.pendingAction.applicationNo, 'G-001')
|
||||||
|
vm.cancelAction()
|
||||||
|
Vue.axios = { get: async url => {
|
||||||
|
assert.equal(url, '/api/v2/admin/gifts/G-001')
|
||||||
|
return { data: envelope(detail()) }
|
||||||
|
} }
|
||||||
|
const button = rendered.find(node => node.tag === 'v-btn' && nodes(node).some(child => (child.text || '').trim() === '상세'))
|
||||||
|
await button.data.on.click()
|
||||||
|
assert.equal(vm.show_detail_dialog, true)
|
||||||
|
assert.equal(vm.detail.applicationNo, 'G-001')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('상세: 수취인 미입력과 배송정보 null을 표시하고 모든 정보를 제공한다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
Vue.axios = { get: async () => ({ data: envelope(detail()) }) }
|
||||||
|
await vm.openDetail('G-001')
|
||||||
|
const text = nodes(vm._render()).map(node => node.text || '').join(' ')
|
||||||
|
for (const label of ['발송인 정보', '수취인 정보', '상품 정보', '입고 배송 정보', '미입력', '발송 확인', 'G-001', '소형', '문구', 'C1']) assert.ok(text.includes(label), label)
|
||||||
|
assert.ok(text.includes('-'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('액션: 확인 전 POST하지 않고 취소/미허용 액션은 실행하지 않는다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
let posts = 0
|
||||||
|
Vue.axios = { post: async () => { posts++; return { data: envelope({}) } } }
|
||||||
|
vm.requestAction(detail(), 'COMPLETE_DELIVERY')
|
||||||
|
assert.equal(vm.show_confirm_dialog, false)
|
||||||
|
vm.requestAction(detail(), 'ARRIVE_MAILBOX')
|
||||||
|
assert.equal(vm.show_confirm_dialog, true)
|
||||||
|
assert.equal(posts, 0)
|
||||||
|
vm.cancelAction()
|
||||||
|
await vm.executeAction()
|
||||||
|
assert.equal(posts, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('전달 불가: 사유 검증 후 확인창을 열고 trim된 사유만 전송한다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const posts = []
|
||||||
|
Vue.axios = {
|
||||||
|
post: async (...args) => { posts.push(args); return { data: envelope({ status: 'UNDELIVERABLE' }) } },
|
||||||
|
get: async () => ({ data: envelope(list()) })
|
||||||
|
}
|
||||||
|
vm.requestAction(detail(), 'MARK_UNDELIVERABLE')
|
||||||
|
assert.equal(vm.show_reason_dialog, true)
|
||||||
|
assert.equal(vm.show_confirm_dialog, false)
|
||||||
|
for (const reason of ['', ' \n ', '가'.repeat(256)]) {
|
||||||
|
vm.reason = reason
|
||||||
|
vm.confirmReason()
|
||||||
|
await vm.executeAction()
|
||||||
|
assert.equal(vm.show_confirm_dialog, false)
|
||||||
|
assert.equal(posts.length, 0)
|
||||||
|
}
|
||||||
|
vm.reason = ' 파손 '
|
||||||
|
vm.confirmReason()
|
||||||
|
assert.equal(vm.show_confirm_dialog, true)
|
||||||
|
assert.equal(posts.length, 0)
|
||||||
|
await vm.executeAction()
|
||||||
|
assert.deepEqual(posts, [['/api/v2/admin/gifts/G-001/mark-undeliverable', { reason: '파손' }]])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('전달 불가: 확인 취소 시 사유를 유지하고 입력 모달 취소 시 초기화한다', () => {
|
||||||
|
const { vm } = page()
|
||||||
|
vm.requestAction(detail(), 'MARK_UNDELIVERABLE')
|
||||||
|
vm.reason = '파손'
|
||||||
|
vm.confirmReason()
|
||||||
|
vm.cancelConfirmation()
|
||||||
|
assert.equal(vm.show_confirm_dialog, false)
|
||||||
|
assert.equal(vm.show_reason_dialog, true)
|
||||||
|
assert.equal(vm.reason, '파손')
|
||||||
|
vm.cancelAction()
|
||||||
|
assert.equal(vm.show_reason_dialog, false)
|
||||||
|
assert.equal(vm.pendingAction, null)
|
||||||
|
assert.equal(vm.reason, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('액션: 각 무본문 액션을 확인 후 실행하며 닫힌 상세는 조회하지 않는다', async () => {
|
||||||
|
const scenarios = [
|
||||||
|
['ARRIVE_MAILBOX', 'arrive-mailbox'], ['COMPLETE_INSPECTION', 'complete-inspection'], ['COMPLETE_DELIVERY', 'complete-delivery']
|
||||||
|
]
|
||||||
|
for (const [action, endpoint] of scenarios) {
|
||||||
|
const { vm, successes } = page()
|
||||||
|
const calls = []
|
||||||
|
Vue.axios = {
|
||||||
|
post: async (...args) => { calls.push(args); return { data: envelope({}) } },
|
||||||
|
get: async (url, config) => {
|
||||||
|
assert.equal(url, '/api/v2/admin/gifts')
|
||||||
|
assert.equal(config.params.page, 0)
|
||||||
|
return { data: envelope(list()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm.requestAction(detail({ availableActions: [action] }), action)
|
||||||
|
await vm.executeAction()
|
||||||
|
assert.deepEqual(calls, [[`/api/v2/admin/gifts/G-001/${endpoint}`]])
|
||||||
|
assert.equal(successes.length, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('성공: 중복 실행을 차단하고 목록과 열린 상세를 재조회한다', async () => {
|
||||||
|
const { vm, successes } = page()
|
||||||
|
const pending = deferred()
|
||||||
|
const calls = []
|
||||||
|
Vue.axios = {
|
||||||
|
post: async (...args) => { calls.push(['POST', ...args]); await pending.promise; return { data: envelope({ status: 'ARRIVED_AT_MAILBOX' }) } },
|
||||||
|
get: async (url, config) => {
|
||||||
|
calls.push(['GET', url, config])
|
||||||
|
return { data: envelope(config ? list({ page: 2, totalCount: 60 }) : detail({ status: 'ARRIVED_AT_MAILBOX', availableActions: ['COMPLETE_INSPECTION', 'MARK_UNDELIVERABLE'] })) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm.appliedFilters = { status: 'TRACKING_REGISTERED', applicationNo: '', nickname: '발송인' }
|
||||||
|
vm.page = 3
|
||||||
|
vm.detail = detail()
|
||||||
|
vm.show_detail_dialog = true
|
||||||
|
vm.requestAction(vm.detail, 'ARRIVE_MAILBOX')
|
||||||
|
const operation = vm.executeAction()
|
||||||
|
await vm.executeAction()
|
||||||
|
pending.resolve()
|
||||||
|
await operation
|
||||||
|
assert.equal(calls.filter(call => call[0] === 'POST').length, 1)
|
||||||
|
assert.deepEqual(calls.find(call => call[1] === '/api/v2/admin/gifts')[2].params, { status: 'TRACKING_REGISTERED', nickname: '발송인', page: 2, size: 20 })
|
||||||
|
assert.ok(calls.some(call => call[1] === '/api/v2/admin/gifts/G-001'))
|
||||||
|
assert.equal(vm.detail.status, 'ARRIVED_AT_MAILBOX')
|
||||||
|
assert.equal(successes.length, 1)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
assert.equal(vm.show_confirm_dialog, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('실패: envelope 실패/HTTP 오류에 공통 오류 알림을 표시한다', async () => {
|
||||||
|
const { vm, errors, successes } = page()
|
||||||
|
Vue.axios = { get: async () => ({ data: { success: false, message: '조회 실패' } }) }
|
||||||
|
await vm.getGifts()
|
||||||
|
assert.ok(errors.includes('조회 실패'))
|
||||||
|
for (const fail of [async () => ({ data: { success: false, message: '상태 변경 실패' } }), async () => { throw { response: { data: { message: '권한 없음' } } } }]) {
|
||||||
|
Vue.axios.post = fail
|
||||||
|
vm.requestAction(detail(), 'ARRIVE_MAILBOX')
|
||||||
|
await vm.executeAction()
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
vm.cancelAction()
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, ['조회 실패', '상태 변경 실패', '권한 없음'])
|
||||||
|
assert.equal(successes.length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('성공 후 재조회 실패: 오래된 액션을 제거하고 성공/조회 실패를 구분한다', async () => {
|
||||||
|
const { vm, errors, successes } = page()
|
||||||
|
vm.detail = detail()
|
||||||
|
vm.show_detail_dialog = true
|
||||||
|
Vue.axios = {
|
||||||
|
post: async () => ({ data: envelope({}) }),
|
||||||
|
get: async () => { throw new Error('조회 실패') }
|
||||||
|
}
|
||||||
|
vm.requestAction(vm.detail, 'ARRIVE_MAILBOX')
|
||||||
|
await vm.executeAction()
|
||||||
|
assert.equal(successes.length, 1)
|
||||||
|
assert.equal(errors.length, 2)
|
||||||
|
assert.equal(vm.detail, null)
|
||||||
|
assert.equal(vm.gifts.length, 0)
|
||||||
|
assert.equal(vm.list_error, true)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
Vue.axios.get = async () => ({ data: envelope(detail({ availableActions: ['COMPLETE_INSPECTION'] })) })
|
||||||
|
await vm.getDetail(vm.detailApplicationNo)
|
||||||
|
assert.deepEqual(Array.from(vm.detail.availableActions), ['COMPLETE_INSPECTION'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('목록: 늦게 도착한 응답이 최신 페이지를 덮어쓰지 않는다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const pending = deferred()
|
||||||
|
Vue.axios = { get: async (url, config) => config.params.page === 0 ? pending.promise : { data: envelope(list({ page: 1, totalCount: 40 })) } }
|
||||||
|
const old = vm.getGifts(1)
|
||||||
|
await vm.getGifts(2)
|
||||||
|
pending.resolve({ data: envelope(list({ page: 0, totalCount: 1 })) })
|
||||||
|
await old
|
||||||
|
assert.equal(vm.page, 2)
|
||||||
|
assert.equal(vm.totalCount, 40)
|
||||||
|
assert.equal(vm.is_loading, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('상세: 뒤늦은 이전 응답이 새로 선택한 선물을 덮어쓰지 않는다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const pending = deferred()
|
||||||
|
Vue.axios = { get: async url => url.endsWith('/OLD') ? pending.promise : { data: envelope(detail({ applicationNo: 'NEW' })) } }
|
||||||
|
const old = vm.openDetail('OLD')
|
||||||
|
await vm.openDetail('NEW')
|
||||||
|
pending.resolve({ data: envelope(detail({ applicationNo: 'OLD' })) })
|
||||||
|
await old
|
||||||
|
assert.equal(vm.detail.applicationNo, 'NEW')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('상세: 조회 중 닫은 모달에 늦은 응답을 반영하지 않는다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const pending = deferred()
|
||||||
|
Vue.axios = { get: async () => pending.promise }
|
||||||
|
const request = vm.openDetail('G-001')
|
||||||
|
vm.closeDetail()
|
||||||
|
pending.resolve({ data: envelope(detail()) })
|
||||||
|
await request
|
||||||
|
assert.equal(vm.show_detail_dialog, false)
|
||||||
|
assert.equal(vm.detail, null)
|
||||||
|
assert.equal(vm.is_detail_loading, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('메뉴: ADMIN에 선물함 리스트를 추가하고 기존 서버 메뉴와 중복되지 않는다', async () => {
|
||||||
|
for (const role of ['ADMIN', 'CONTENT_MANAGER']) {
|
||||||
|
for (const existing of [false, true]) {
|
||||||
|
const menus = [{ title: '콘텐츠 리스트', route: '/content/list' }]
|
||||||
|
if (existing) menus.push({ title: '선물함 관리', items: [{ title: '선물함 리스트', route: '/gift/list' }] })
|
||||||
|
Vue.axios = { get: async () => ({ status: 200, data: envelope(menus) }) }
|
||||||
|
const vm = new Vue({ ...SideMenu, created: undefined })
|
||||||
|
vm.$store = { state: { accountStore: { role } } }
|
||||||
|
await vm.getMenus()
|
||||||
|
const gifts = vm.items.filter(item => item.title === '선물함 관리')
|
||||||
|
assert.equal(gifts.length, role === 'ADMIN' || existing ? 1 : 0)
|
||||||
|
if (gifts.length) assert.equal(gifts[0].items.filter(item => item.route === '/gift/list').length, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
for (const { name, run } of tests) {
|
||||||
|
try {
|
||||||
|
await run()
|
||||||
|
console.log(`PASS ${name}`)
|
||||||
|
} catch (error) {
|
||||||
|
process.exitCode = 1
|
||||||
|
console.error(`FAIL ${name}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
const assert = require('assert').strict
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const Module = require('module')
|
||||||
|
const { transformSync } = require('@babel/core')
|
||||||
|
const compiler = require('vue-template-compiler')
|
||||||
|
const Vue = require('vue')
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..')
|
||||||
|
const originalResolve = Module._resolveFilename
|
||||||
|
Module._resolveFilename = function(request, parent, ...rest) {
|
||||||
|
return originalResolve.call(this, request.startsWith('@/') ? path.join(root, 'src', request.slice(2)) : request, parent, ...rest)
|
||||||
|
}
|
||||||
|
const originalJs = require.extensions['.js']
|
||||||
|
function compileScript(source, filename) {
|
||||||
|
return transformSync(source, {
|
||||||
|
filename,
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
plugins: ['@babel/plugin-transform-modules-commonjs']
|
||||||
|
}).code
|
||||||
|
}
|
||||||
|
require.extensions['.js'] = (module, filename) => {
|
||||||
|
if (!filename.startsWith(path.join(root, 'src') + path.sep)) return originalJs(module, filename)
|
||||||
|
module._compile(compileScript(fs.readFileSync(filename, 'utf8'), filename), filename)
|
||||||
|
}
|
||||||
|
require.extensions['.vue'] = (module, filename) => {
|
||||||
|
const { script, template } = compiler.parseComponent(fs.readFileSync(filename, 'utf8'))
|
||||||
|
const compiled = compiler.compile(template.content)
|
||||||
|
assert.deepEqual(compiled.errors, [], `${filename}: 템플릿 컴파일`)
|
||||||
|
module._compile(compileScript(script.content, filename), filename)
|
||||||
|
Object.assign(module.exports.default, compiler.compileToFunctions(template.content))
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = require('../src/api/gift_category')
|
||||||
|
const { createGiftCategoryForm, buildGiftCategoryRequest, validateGiftCategory } = require('../src/utils/gift_category')
|
||||||
|
const GiftCategory = require('../src/views/Gift/GiftCategory.vue').default
|
||||||
|
const SideMenu = require('../src/components/SideMenu.vue').default
|
||||||
|
const tests = []
|
||||||
|
const test = (name, run) => tests.push({ name, run })
|
||||||
|
const envelope = data => ({ success: true, data })
|
||||||
|
const category = (overrides = {}) => ({
|
||||||
|
categoryId: 1, classificationNumber: '100', categoryCode: 'DOLL', name: '인형',
|
||||||
|
receiptCode: 'A', representativeItem: '피규어', requiresDamageWaiver: false, isActive: true,
|
||||||
|
...overrides
|
||||||
|
})
|
||||||
|
const request = (overrides = {}) => ({
|
||||||
|
classificationNumber: '100', categoryCode: 'DOLL', name: '인형', receiptCode: 'A',
|
||||||
|
representativeItem: '피규어', requiresDamageWaiver: false, isActive: true,
|
||||||
|
...overrides
|
||||||
|
})
|
||||||
|
function page() {
|
||||||
|
const errors = []
|
||||||
|
const successes = []
|
||||||
|
const vm = new Vue({ ...GiftCategory, created: undefined })
|
||||||
|
vm.$dialog = { notify: { error: message => errors.push(message), success: message => successes.push(message) } }
|
||||||
|
return { vm, errors, successes }
|
||||||
|
}
|
||||||
|
function deferred() {
|
||||||
|
let resolve
|
||||||
|
const promise = new Promise(done => { resolve = done })
|
||||||
|
return { promise, resolve }
|
||||||
|
}
|
||||||
|
function nodes(vnode) {
|
||||||
|
if (!vnode) return []
|
||||||
|
return [vnode].concat((vnode.children || (vnode.componentOptions && vnode.componentOptions.children) || []).flatMap(nodes))
|
||||||
|
}
|
||||||
|
const text = vnode => nodes(vnode).map(node => node.text || '').join('').trim()
|
||||||
|
const button = (vm, label) => nodes(vm._render()).find(node => node.tag === 'v-btn' && text(node) === label)
|
||||||
|
|
||||||
|
test('카테고리 API: GET/POST/PUT/DELETE 경로, 본문 및 공통 envelope를 사용한다', async () => {
|
||||||
|
const calls = []
|
||||||
|
const response = envelope(category())
|
||||||
|
Vue.axios = Object.fromEntries(['get', 'post', 'put', 'delete'].map(method => [method, async (...args) => {
|
||||||
|
calls.push([method, ...args])
|
||||||
|
return { data: response }
|
||||||
|
}]))
|
||||||
|
assert.equal(await api.getGiftCategories(), response)
|
||||||
|
assert.equal(await api.createGiftCategory(category({ extra: '제외' })), response)
|
||||||
|
assert.equal(await api.updateGiftCategory(1, category({ extra: '제외' })), response)
|
||||||
|
assert.equal(await api.deactivateGiftCategory(1), response)
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['get', '/api/v2/admin/gift-categories'],
|
||||||
|
['post', '/api/v2/admin/gift-categories', request()],
|
||||||
|
['put', '/api/v2/admin/gift-categories/1', request()],
|
||||||
|
['delete', '/api/v2/admin/gift-categories/1']
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 validation: 필수/형식/길이/boolean 오류를 검사한다', () => {
|
||||||
|
const invalid = {
|
||||||
|
classificationNumber: ['', ' ', null, undefined, 100, '10', '1000', 'A10', '123', '100\n', ' 10'],
|
||||||
|
categoryCode: ['', ' \t ', null, undefined, 1, 'A'.repeat(51)],
|
||||||
|
name: ['', ' \n ', null, undefined, 1, '가'.repeat(51)],
|
||||||
|
receiptCode: ['', ' ', null, undefined, 1, 'a', 'Aa', 'A1', '가', 'A B', 'A\n', 'A'.repeat(10)],
|
||||||
|
representativeItem: ['', ' ', null, undefined, 1, '가'.repeat(101)],
|
||||||
|
requiresDamageWaiver: [null, undefined, '', 'false', 0, 1],
|
||||||
|
isActive: [null, undefined, '', 'true', 0, 1]
|
||||||
|
}
|
||||||
|
for (const [key, values] of Object.entries(invalid)) {
|
||||||
|
for (const value of values) {
|
||||||
|
const errors = validateGiftCategory(request({ [key]: value }))
|
||||||
|
assert.deepEqual(Object.keys(errors), [key], `${key}: ${String(value)}`)
|
||||||
|
assert.equal(typeof errors[key], 'string')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 validation: 최대 길이/앞자리 0/false를 보존하며 ID는 본문에서 제외한다', () => {
|
||||||
|
for (const requiresDamageWaiver of [true, false]) {
|
||||||
|
for (const isActive of [true, false]) {
|
||||||
|
const form = request({
|
||||||
|
classificationNumber: '001', categoryCode: 'A'.repeat(50), name: '가'.repeat(50),
|
||||||
|
receiptCode: 'ABCDEFGHI', representativeItem: '가'.repeat(100), requiresDamageWaiver, isActive
|
||||||
|
})
|
||||||
|
assert.deepEqual(validateGiftCategory(form), {})
|
||||||
|
assert.deepEqual(buildGiftCategoryRequest({ categoryId: 7, extra: '제외', ...form }), form)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const first = createGiftCategoryForm()
|
||||||
|
first.name = '변경'
|
||||||
|
assert.equal(createGiftCategoryForm().name, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 목록: 최초 GET의 response.data 배열과 필수 9개 컬럼을 표시한다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const calls = []
|
||||||
|
Vue.axios = { get: async (...args) => { calls.push(args); return { data: envelope([category()]) } } }
|
||||||
|
await GiftCategory.created.call(vm)
|
||||||
|
assert.deepEqual(calls, [['/api/v2/admin/gift-categories']])
|
||||||
|
assert.deepEqual(Array.from(vm.categories), [category()])
|
||||||
|
const rendered = nodes(vm._render())
|
||||||
|
assert.deepEqual(rendered.filter(node => node.tag === 'th').map(text), [
|
||||||
|
'카테고리 ID', '분류번호', '카테고리 코드', '카테고리명', '접수 코드', '대표 품목', '파손면책 동의 필요 여부', '활성 여부', '관리'
|
||||||
|
])
|
||||||
|
assert.deepEqual(rendered.filter(node => node.tag === 'td').slice(0, 8).map(text), ['1', '100', 'DOLL', '인형', 'A', '피규어', '아니오', '활성'])
|
||||||
|
button(vm, '수정').data.on.click()
|
||||||
|
assert.equal(vm.isEdit, true)
|
||||||
|
vm.closeForm()
|
||||||
|
button(vm, '카테고리 등록').data.on.click()
|
||||||
|
assert.equal(vm.isEdit, false)
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 목록: 빈 목록, 로딩 및 오류 재조회 상태를 표시한다', async () => {
|
||||||
|
const { vm, errors } = page()
|
||||||
|
assert.ok(text(vm._render()).includes('등록된 카테고리가 없습니다.'))
|
||||||
|
const pending = deferred()
|
||||||
|
Vue.axios = { get: async () => pending.promise }
|
||||||
|
const load = vm.getCategories()
|
||||||
|
assert.equal(vm.is_loading, true)
|
||||||
|
assert.ok(text(vm._render()).includes('조회 중입니다.'))
|
||||||
|
assert.equal(button(vm, '카테고리 등록').data.attrs.disabled, true)
|
||||||
|
pending.resolve({ data: { success: false, message: '조회 실패' } })
|
||||||
|
await load
|
||||||
|
assert.deepEqual(errors, ['조회 실패'])
|
||||||
|
assert.equal(vm.is_loading, false)
|
||||||
|
assert.equal(vm.list_error, true)
|
||||||
|
Vue.axios.get = async () => ({ data: envelope([]) })
|
||||||
|
await button(vm, '다시 조회').data.on.click()
|
||||||
|
assert.equal(vm.list_error, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 폼: 수정 코드는 읽기 전용이며 취소는 목록을 변경하지 않고 등록은 초기화한다', () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const item = category()
|
||||||
|
vm.openEdit(item)
|
||||||
|
assert.equal(vm.categoryId, 1)
|
||||||
|
assert.deepEqual(vm.form, request())
|
||||||
|
const fields = nodes(vm._render()).filter(node => node.tag === 'v-text-field')
|
||||||
|
assert.equal(fields.length, 5)
|
||||||
|
for (const field of fields) assert.equal(field.data.attrs.readonly, field.data.attrs.label === '카테고리 코드')
|
||||||
|
assert.equal(nodes(vm._render()).filter(node => node.tag === 'v-switch').length, 2)
|
||||||
|
vm.form.name = '변경'
|
||||||
|
vm.form.isActive = false
|
||||||
|
vm.closeForm()
|
||||||
|
assert.equal(item.name, '인형')
|
||||||
|
assert.equal(item.isActive, true)
|
||||||
|
vm.form_errors = { name: '오류' }
|
||||||
|
vm.openCreate()
|
||||||
|
assert.equal(vm.categoryId, null)
|
||||||
|
assert.deepEqual(vm.form, createGiftCategoryForm())
|
||||||
|
assert.deepEqual(vm.form_errors, {})
|
||||||
|
assert.equal(nodes(vm._render()).find(node => node.tag === 'v-text-field' && node.data.attrs.label === '카테고리 코드').data.attrs.readonly, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 폼: validation 실패 시 등록/수정 API 없이 필드 오류를 표시한다', async () => {
|
||||||
|
for (const edit of [false, true]) {
|
||||||
|
const { vm, successes } = page()
|
||||||
|
const calls = []
|
||||||
|
Vue.axios = { post: async () => { calls.push('POST') }, put: async () => { calls.push('PUT') } }
|
||||||
|
if (edit) vm.openEdit(category())
|
||||||
|
else vm.openCreate()
|
||||||
|
vm.form = request({ classificationNumber: '10', categoryCode: '', name: '', receiptCode: 'a', representativeItem: '', requiresDamageWaiver: null, isActive: null })
|
||||||
|
const form = nodes(vm._render()).find(node => node.tag === 'v-form')
|
||||||
|
await form.data.on.submit({ preventDefault() {} })
|
||||||
|
assert.deepEqual(calls, [])
|
||||||
|
assert.deepEqual(successes, [])
|
||||||
|
assert.equal(Object.keys(vm.form_errors).length, 7)
|
||||||
|
for (const field of nodes(vm._render()).filter(node => ['v-text-field', 'v-switch'].includes(node.tag))) {
|
||||||
|
assert.ok(field.data.attrs['error-messages'])
|
||||||
|
}
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 저장: POST/PUT 성공 후 재조회하고 모달을 닫으며 중복 제출을 차단한다', async () => {
|
||||||
|
for (const edit of [false, true]) {
|
||||||
|
const { vm, successes } = page()
|
||||||
|
const pending = deferred()
|
||||||
|
const calls = []
|
||||||
|
const payload = request({ classificationNumber: '001', requiresDamageWaiver: true, isActive: false })
|
||||||
|
const saved = category(payload)
|
||||||
|
Vue.axios = {
|
||||||
|
[edit ? 'put' : 'post']: async (...args) => { calls.push([edit ? 'PUT' : 'POST', ...args]); return { data: envelope(saved) } },
|
||||||
|
get: async (...args) => { calls.push(['GET', ...args]); return pending.promise }
|
||||||
|
}
|
||||||
|
if (edit) vm.openEdit(category())
|
||||||
|
else vm.openCreate()
|
||||||
|
vm.form = { ...payload, categoryId: 99, extra: '제외' }
|
||||||
|
const operation = vm.saveCategory()
|
||||||
|
await vm.saveCategory()
|
||||||
|
await Vue.nextTick()
|
||||||
|
assert.equal(vm.is_operating, true)
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
vm.closeForm()
|
||||||
|
vm.openEdit(category({ categoryId: 2 }))
|
||||||
|
vm.openCreate()
|
||||||
|
assert.equal(vm.categoryId, edit ? 1 : null)
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
pending.resolve({ data: envelope([saved]) })
|
||||||
|
await operation
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
[edit ? 'PUT' : 'POST', `/api/v2/admin/gift-categories${edit ? '/1' : ''}`, payload],
|
||||||
|
['GET', '/api/v2/admin/gift-categories']
|
||||||
|
])
|
||||||
|
assert.deepEqual(Array.from(vm.categories), [saved])
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
assert.deepEqual(successes, [edit ? '카테고리가 수정되었습니다.' : '카테고리가 등록되었습니다.'])
|
||||||
|
await vm.saveCategory()
|
||||||
|
assert.equal(calls.length, 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 저장 실패: 공통 오류 알림과 입력/모달을 유지한다', async () => {
|
||||||
|
for (const edit of [false, true]) {
|
||||||
|
for (const fail of [
|
||||||
|
async () => ({ data: { success: false, message: '저장 실패' } }),
|
||||||
|
async () => { throw { response: { data: { message: '저장 실패' } } } },
|
||||||
|
async () => { throw new Error('저장 실패') }
|
||||||
|
]) {
|
||||||
|
const { vm, errors, successes } = page()
|
||||||
|
let loads = 0
|
||||||
|
Vue.axios = { post: fail, put: fail, get: async () => { loads++ } }
|
||||||
|
if (edit) vm.openEdit(category())
|
||||||
|
else vm.openCreate()
|
||||||
|
vm.form = request()
|
||||||
|
await vm.saveCategory()
|
||||||
|
assert.deepEqual(errors, ['저장 실패'])
|
||||||
|
assert.deepEqual(successes, [])
|
||||||
|
assert.deepEqual(vm.form, request())
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
assert.equal(loads, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 저장 후 조회 실패: 저장 성공과 조회 오류를 구분하며 중복 등록을 방지한다', async () => {
|
||||||
|
const { vm, errors, successes } = page()
|
||||||
|
Vue.axios = {
|
||||||
|
post: async () => ({ data: envelope(category()) }),
|
||||||
|
get: async () => { throw new Error('조회 실패') }
|
||||||
|
}
|
||||||
|
vm.openCreate()
|
||||||
|
vm.form = request()
|
||||||
|
await vm.saveCategory()
|
||||||
|
assert.deepEqual(successes, ['카테고리가 등록되었습니다.'])
|
||||||
|
assert.deepEqual(errors, ['조회 실패'])
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
assert.equal(vm.list_error, true)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 비활성화: 확인 전/취소 후/이미 비활성이면 DELETE하지 않는다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
let calls = 0
|
||||||
|
Vue.axios = { delete: async () => { calls++ } }
|
||||||
|
vm.categories = [category()]
|
||||||
|
button(vm, '비활성화').data.on.click()
|
||||||
|
assert.equal(vm.show_confirm_dialog, true)
|
||||||
|
assert.ok(text(vm._render()).includes('인형 카테고리를 비활성화하시겠습니까?'))
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
vm.cancelDeactivation()
|
||||||
|
await vm.deactivateCategory()
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(vm.pendingCategory, null)
|
||||||
|
vm.categories = [category({ isActive: false, requiresDamageWaiver: true })]
|
||||||
|
assert.equal(button(vm, '비활성화').data.attrs.disabled, true)
|
||||||
|
assert.ok(text(vm._render()).includes('비활성'))
|
||||||
|
assert.ok(text(vm._render()).includes('예'))
|
||||||
|
vm.requestDeactivation(vm.categories[0])
|
||||||
|
await vm.deactivateCategory()
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(vm.show_confirm_dialog, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 비활성화 성공: 응답의 isActive=false를 반영하고 row는 유지한다', async () => {
|
||||||
|
const { vm, successes } = page()
|
||||||
|
const pending = deferred()
|
||||||
|
const calls = []
|
||||||
|
Vue.axios = { delete: async (...args) => { calls.push(args); return pending.promise } }
|
||||||
|
vm.categories = [category(), category({ categoryId: 2 })]
|
||||||
|
vm.requestDeactivation(vm.categories[0])
|
||||||
|
const operation = button(vm, '확인').data.on.click()
|
||||||
|
await vm.deactivateCategory()
|
||||||
|
vm.cancelDeactivation()
|
||||||
|
assert.equal(vm.show_confirm_dialog, true)
|
||||||
|
assert.equal(vm.is_operating, true)
|
||||||
|
assert.equal(button(vm, '비활성화').data.attrs.disabled, true)
|
||||||
|
pending.resolve({ data: envelope(category({ isActive: false })) })
|
||||||
|
await operation
|
||||||
|
assert.deepEqual(calls, [['/api/v2/admin/gift-categories/1']])
|
||||||
|
assert.deepEqual(Array.from(vm.categories), [category({ isActive: false }), category({ categoryId: 2 })])
|
||||||
|
assert.equal(button(vm, '비활성화').data.attrs.disabled, true)
|
||||||
|
assert.equal(vm.show_confirm_dialog, false)
|
||||||
|
assert.equal(vm.pendingCategory, null)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
assert.deepEqual(successes, ['카테고리가 비활성화되었습니다.'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 비활성화 실패: 기존 활성 상태를 유지하고 공통 오류를 표시한다', async () => {
|
||||||
|
for (const fail of [
|
||||||
|
async () => ({ data: { success: false, message: '비활성화 실패' } }),
|
||||||
|
async () => { throw { response: { data: { message: '비활성화 실패' } } } }
|
||||||
|
]) {
|
||||||
|
const { vm, errors, successes } = page()
|
||||||
|
Vue.axios = { delete: fail }
|
||||||
|
vm.categories = [category()]
|
||||||
|
vm.requestDeactivation(vm.categories[0])
|
||||||
|
await vm.deactivateCategory()
|
||||||
|
assert.equal(vm.categories[0].isActive, true)
|
||||||
|
assert.equal(vm.show_confirm_dialog, true)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
assert.deepEqual(errors, ['비활성화 실패'])
|
||||||
|
assert.deepEqual(successes, [])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 목록: 늦게 도착한 이전 응답은 최신 목록을 덮어쓰지 않는다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const pending = deferred()
|
||||||
|
Vue.axios = { get: async () => pending.promise }
|
||||||
|
const old = vm.getCategories()
|
||||||
|
Vue.axios.get = async () => ({ data: envelope([category({ categoryId: 2 })]) })
|
||||||
|
await vm.getCategories()
|
||||||
|
pending.resolve({ data: envelope([category()]) })
|
||||||
|
await old
|
||||||
|
assert.deepEqual(Array.from(vm.categories), [category({ categoryId: 2 })])
|
||||||
|
assert.equal(vm.is_loading, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('카테고리 메뉴: ADMIN 보완 메뉴와 서버 메뉴를 중복 없이 유지한다', async () => {
|
||||||
|
for (const role of ['ADMIN', 'CONTENT_MANAGER']) {
|
||||||
|
for (const existing of ['none', 'null', 'list', 'category', 'object']) {
|
||||||
|
const menus = []
|
||||||
|
if (existing !== 'none') {
|
||||||
|
const items = existing === 'null' ? null : existing === 'list'
|
||||||
|
? [{ title: '선물함 리스트', route: '/gift/list' }]
|
||||||
|
: existing === 'object' ? { title: '선물 카테고리', route: '/gift/category' }
|
||||||
|
: [{ title: '선물 카테고리', route: '/gift/category' }]
|
||||||
|
menus.push({ title: '선물함 관리', items })
|
||||||
|
}
|
||||||
|
Vue.axios = { get: async () => ({ status: 200, data: envelope(menus) }) }
|
||||||
|
const vm = new Vue({ ...SideMenu, created: undefined })
|
||||||
|
vm.$store = { state: { accountStore: { role } } }
|
||||||
|
await vm.getMenus()
|
||||||
|
const gifts = vm.items.filter(item => item.title === '선물함 관리')
|
||||||
|
assert.equal(gifts.length, role === 'ADMIN' || existing !== 'none' ? 1 : 0)
|
||||||
|
const children = gifts.length && gifts[0].items ? [].concat(gifts[0].items) : []
|
||||||
|
assert.equal(children.filter(item => item.route === '/gift/category').length, role === 'ADMIN' || ['category', 'object'].includes(existing) ? 1 : 0)
|
||||||
|
if (role === 'ADMIN') assert.equal(children.filter(item => item.route === '/gift/list').length, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
for (const { name, run } of tests) {
|
||||||
|
try {
|
||||||
|
await run()
|
||||||
|
console.log(`PASS ${name}`)
|
||||||
|
} catch (error) {
|
||||||
|
process.exitCode = 1
|
||||||
|
console.error(`FAIL ${name}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
@@ -0,0 +1,602 @@
|
|||||||
|
const assert = require('assert').strict
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const Module = require('module')
|
||||||
|
const { transformSync } = require('@babel/core')
|
||||||
|
const compiler = require('vue-template-compiler')
|
||||||
|
const Vue = require('vue')
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..')
|
||||||
|
const originalResolve = Module._resolveFilename
|
||||||
|
Module._resolveFilename = function(request, parent, ...rest) {
|
||||||
|
return originalResolve.call(this, request.startsWith('@/') ? path.join(root, 'src', request.slice(2)) : request, parent, ...rest)
|
||||||
|
}
|
||||||
|
const originalJs = require.extensions['.js']
|
||||||
|
function compileScript(source, filename) {
|
||||||
|
return transformSync(source, {
|
||||||
|
filename,
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
plugins: ['@babel/plugin-transform-modules-commonjs']
|
||||||
|
}).code
|
||||||
|
}
|
||||||
|
require.extensions['.js'] = (module, filename) => {
|
||||||
|
if (!filename.startsWith(path.join(root, 'src') + path.sep)) return originalJs(module, filename)
|
||||||
|
module._compile(compileScript(fs.readFileSync(filename, 'utf8'), filename), filename)
|
||||||
|
}
|
||||||
|
require.extensions['.vue'] = (module, filename) => {
|
||||||
|
const { script, template } = compiler.parseComponent(fs.readFileSync(filename, 'utf8'))
|
||||||
|
const compiled = compiler.compile(template.content)
|
||||||
|
assert.deepEqual(compiled.errors, [], `${filename}: 템플릿 컴파일`)
|
||||||
|
module._compile(compileScript(script.content, filename), filename)
|
||||||
|
Object.assign(module.exports.default, compiler.compileToFunctions(template.content))
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = require('../src/api/gift_size')
|
||||||
|
const { giftSizes, createGiftSizeForm, buildGiftSizeRequest, validateGiftSize } = require('../src/utils/gift_size')
|
||||||
|
const GiftSize = require('../src/views/Gift/GiftSize.vue').default
|
||||||
|
const SideMenu = require('../src/components/SideMenu.vue').default
|
||||||
|
const tests = []
|
||||||
|
const test = (name, run) => tests.push({ name, run })
|
||||||
|
const envelope = data => ({ success: true, data })
|
||||||
|
const size = (overrides = {}) => ({ sizeCode: 'SMALL', name: '소형', basePriceCan: 12000, salePriceCan: 10000, isActive: true, ...overrides })
|
||||||
|
const request = (overrides = {}) => ({ basePriceCan: 12000, salePriceCan: 10000, isActive: true, ...overrides })
|
||||||
|
function page() {
|
||||||
|
const errors = []
|
||||||
|
const successes = []
|
||||||
|
const confirmations = []
|
||||||
|
const vm = new Vue({ ...GiftSize, created: undefined })
|
||||||
|
vm.$dialog = {
|
||||||
|
notify: { error: message => errors.push(message), success: message => successes.push(message) },
|
||||||
|
confirm: async options => { confirmations.push(options); return true }
|
||||||
|
}
|
||||||
|
return { vm, errors, successes, confirmations }
|
||||||
|
}
|
||||||
|
function deferred() {
|
||||||
|
let resolve
|
||||||
|
const promise = new Promise(done => { resolve = done })
|
||||||
|
return { promise, resolve }
|
||||||
|
}
|
||||||
|
function nodes(vnode) {
|
||||||
|
if (!vnode) return []
|
||||||
|
return [vnode].concat((vnode.children || (vnode.componentOptions && vnode.componentOptions.children) || []).flatMap(nodes))
|
||||||
|
}
|
||||||
|
const text = vnode => nodes(vnode).map(node => node.text || '').join('').trim()
|
||||||
|
const button = (vm, label) => nodes(vm._render()).find(node => node.tag === 'v-btn' && text(node) === label)
|
||||||
|
|
||||||
|
test('사이즈 API: 유니크한 GiftSize 3종의 GET/PUT 경로와 본문 및 envelope가 일치한다', async () => {
|
||||||
|
assert.deepEqual(giftSizes, ['SMALL', 'MEDIUM', 'LARGE'])
|
||||||
|
assert.equal(new Set(giftSizes).size, 3)
|
||||||
|
assert.deepEqual(Object.keys(api).sort(), ['getGiftSizePrices', 'updateGiftSizePrice'])
|
||||||
|
const calls = []
|
||||||
|
const response = envelope(size())
|
||||||
|
Vue.axios = Object.fromEntries(['get', 'put'].map(method => [method, async (...args) => {
|
||||||
|
calls.push([method, ...args])
|
||||||
|
return { data: response }
|
||||||
|
}]))
|
||||||
|
assert.equal(await api.getGiftSizePrices(), response)
|
||||||
|
for (const sizeCode of giftSizes) {
|
||||||
|
assert.equal(await api.updateGiftSizePrice(sizeCode, size({ basePriceCan: '12000', salePriceCan: '10000', isActive: false, extra: '제외' })), response)
|
||||||
|
}
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['get', '/api/v2/admin/gift-size-prices'],
|
||||||
|
...giftSizes.map(code => ['put', `/api/v2/admin/gift-size-prices/${code}`, request({ isActive: false })])
|
||||||
|
])
|
||||||
|
for (const code of ['S', 'M', 'L', 'small', '', 'XLARGE', null, undefined]) {
|
||||||
|
await assert.rejects(api.updateGiftSizePrice(code, size()))
|
||||||
|
}
|
||||||
|
assert.equal(calls.length, 4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 validation: 필수·1 이상 정수·안전한 숫자·boolean·enum을 검사한다', () => {
|
||||||
|
const invalidPrices = ['', ' ', null, undefined, 0, '0', -1, '-1', 1.1, '1.1', 'abc', NaN, Infinity, 'Infinity', true, false, [], {}, Number.MAX_SAFE_INTEGER + 1]
|
||||||
|
for (const field of ['basePriceCan', 'salePriceCan']) {
|
||||||
|
for (const value of invalidPrices) {
|
||||||
|
const errors = validateGiftSize(size({ [field]: value }))
|
||||||
|
assert.deepEqual(Object.keys(errors), [field], `${field}: ${String(value)}`)
|
||||||
|
assert.ok(errors[field])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const code of ['S', 'M', 'L', 'small', '', null, undefined, 'XLARGE']) {
|
||||||
|
assert.deepEqual(Object.keys(validateGiftSize(size({ sizeCode: code }))), ['sizeCode'])
|
||||||
|
}
|
||||||
|
for (const value of [null, undefined, 'true', 'false', 0, 1]) {
|
||||||
|
assert.deepEqual(Object.keys(validateGiftSize(size({ isActive: value }))), ['isActive'])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 validation: 최소값·동일 가격·숫자 문자열·false를 허용하고 초과 판매가는 거부한다', () => {
|
||||||
|
for (const sizeCode of giftSizes) {
|
||||||
|
for (const price of [1, '1', Number.MAX_SAFE_INTEGER]) {
|
||||||
|
const item = size({ sizeCode, basePriceCan: price, salePriceCan: price, isActive: false })
|
||||||
|
assert.deepEqual(validateGiftSize(item), {})
|
||||||
|
assert.deepEqual(buildGiftSizeRequest({ ...item, extra: '제외' }), { basePriceCan: Number(price), salePriceCan: Number(price), isActive: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.deepEqual(Object.keys(validateGiftSize(size({ basePriceCan: '9', salePriceCan: '10' }))), ['salePriceCan'])
|
||||||
|
const form = createGiftSizeForm(size())
|
||||||
|
assert.deepEqual(form, size())
|
||||||
|
form.basePriceCan = 1
|
||||||
|
assert.equal(createGiftSizeForm(size()).basePriceCan, 12000)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 목록: 최초 GET, 6개 컬럼, 숫자 포맷과 수정 전용 버튼을 표시한다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const rows = giftSizes.map(sizeCode => size({ sizeCode, isActive: sizeCode !== 'LARGE' }))
|
||||||
|
const expected = rows.map(item => ({ ...item }))
|
||||||
|
const calls = []
|
||||||
|
Vue.axios = { get: async (...args) => { calls.push(args); return { data: envelope(rows) } } }
|
||||||
|
await GiftSize.created.call(vm)
|
||||||
|
assert.deepEqual(calls, [['/api/v2/admin/gift-size-prices']])
|
||||||
|
assert.deepEqual(Array.from(vm.sizes), expected)
|
||||||
|
const rendered = nodes(vm._render())
|
||||||
|
assert.equal(text(rendered.find(node => node.tag === 'v-toolbar-title')), '선물 사이즈')
|
||||||
|
assert.deepEqual(rendered.filter(node => node.tag === 'th').map(text), ['사이즈 코드', '사이즈명', '기본가', '판매가', '활성 여부', '관리'])
|
||||||
|
assert.deepEqual(rendered.filter(node => node.tag === 'td').slice(0, 6).map(text), ['SMALL', '소형', (12000).toLocaleString(), (10000).toLocaleString(), '활성', '수정'])
|
||||||
|
assert.ok(text(vm._render()).includes('비활성'))
|
||||||
|
assert.deepEqual(rendered.filter(node => node.tag === 'v-btn').map(text), ['수정', '수정', '수정', '취소', '저장'])
|
||||||
|
button(vm, '수정').data.on.click()
|
||||||
|
assert.deepEqual(vm.form, rows[0])
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 목록: 빈 목록·로딩·실패·재조회 및 늦은 응답을 처리한다', async () => {
|
||||||
|
const { vm, errors } = page()
|
||||||
|
assert.ok(text(vm._render()).includes('등록된 선물 사이즈가 없습니다.'))
|
||||||
|
const pending = deferred()
|
||||||
|
Vue.axios = { get: async () => pending.promise }
|
||||||
|
const old = vm.getSizes()
|
||||||
|
assert.equal(vm.is_loading, true)
|
||||||
|
assert.ok(text(vm._render()).includes('조회 중입니다.'))
|
||||||
|
vm.openEdit(size())
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
Vue.axios.get = async () => ({ data: { success: false, message: '조회 실패' } })
|
||||||
|
await vm.getSizes()
|
||||||
|
assert.deepEqual(errors, ['조회 실패'])
|
||||||
|
assert.equal(vm.list_error, true)
|
||||||
|
Vue.axios.get = async () => ({ data: envelope([size()]) })
|
||||||
|
await button(vm, '다시 조회').data.on.click()
|
||||||
|
pending.resolve({ data: envelope([]) })
|
||||||
|
await old
|
||||||
|
assert.deepEqual(Array.from(vm.sizes), [size()])
|
||||||
|
assert.equal(vm.list_error, false)
|
||||||
|
assert.equal(vm.is_loading, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 최초 등록: 빈 목록에서 고정 3종만 선택하며 가격과 활성 여부를 입력한다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
Vue.axios = { get: async () => ({ data: envelope([]) }) }
|
||||||
|
await GiftSize.created.call(vm)
|
||||||
|
const create = button(vm, '사이즈 등록')
|
||||||
|
assert.ok(create, '빈 목록에서도 최초 등록 버튼이 있어야 한다')
|
||||||
|
assert.equal(create.data.attrs.disabled, false)
|
||||||
|
create.data.on.click()
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.deepEqual(vm.form, createGiftSizeForm())
|
||||||
|
const rendered = nodes(vm._render())
|
||||||
|
assert.ok(text(vm._render()).includes('선물 사이즈 등록'))
|
||||||
|
const select = rendered.find(node => node.tag === 'v-select')
|
||||||
|
assert.ok(select)
|
||||||
|
assert.equal(select.data.attrs.label, '사이즈 코드')
|
||||||
|
assert.deepEqual(Array.from(select.data.attrs.items), ['SMALL', 'MEDIUM', 'LARGE'])
|
||||||
|
select.data.model.callback('MEDIUM')
|
||||||
|
assert.equal(vm.form.sizeCode, 'MEDIUM')
|
||||||
|
const prices = rendered.filter(node => node.tag === 'v-text-field')
|
||||||
|
assert.deepEqual(prices.map(node => node.data.attrs.label), ['기본가', '판매가'])
|
||||||
|
assert.ok(prices.every(node => node.data.attrs.type === 'number'))
|
||||||
|
assert.equal(rendered.filter(node => node.tag === 'v-switch').length, 1)
|
||||||
|
assert.equal(button(vm, '삭제'), undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 최초 등록: 비활성을 포함한 등록 코드는 제외하고 3종 모두 등록되면 추가할 수 없다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
Vue.axios = { get: async () => ({ data: envelope([size({ isActive: false }), size({ sizeCode: 'LARGE' })]) }) }
|
||||||
|
await vm.getSizes()
|
||||||
|
button(vm, '사이즈 등록').data.on.click()
|
||||||
|
const select = nodes(vm._render()).find(node => node.tag === 'v-select')
|
||||||
|
assert.deepEqual(Array.from(select.data.attrs.items), ['MEDIUM'])
|
||||||
|
vm.closeForm()
|
||||||
|
Vue.axios.get = async () => ({ data: envelope(giftSizes.map(sizeCode => size({ sizeCode, isActive: false }))) })
|
||||||
|
await vm.getSizes()
|
||||||
|
assert.equal(button(vm, '사이즈 등록'), undefined)
|
||||||
|
vm.openCreate()
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
assert.equal(nodes(vm._render()).filter(node => node.tag === 'v-btn' && text(node) === '수정').length, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 최초 등록: 최초 조회 전·조회 중·조회 실패에는 등록을 차단하고 재조회 후 허용한다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
assert.equal(button(vm, '사이즈 등록').data.attrs.disabled, true)
|
||||||
|
vm.openCreate()
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
const pending = deferred()
|
||||||
|
Vue.axios = { get: async () => pending.promise }
|
||||||
|
const load = vm.getSizes()
|
||||||
|
assert.equal(button(vm, '사이즈 등록').data.attrs.disabled, true)
|
||||||
|
vm.openCreate()
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
pending.resolve({ data: { success: false, message: '조회 실패' } })
|
||||||
|
await load
|
||||||
|
assert.equal(button(vm, '사이즈 등록').data.attrs.disabled, true)
|
||||||
|
vm.openCreate()
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
Vue.axios.get = async () => ({ data: envelope([]) })
|
||||||
|
await button(vm, '다시 조회').data.on.click()
|
||||||
|
assert.equal(button(vm, '사이즈 등록').data.attrs.disabled, false)
|
||||||
|
vm.openCreate()
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 최초 등록: 미선택·잘못된 코드·기등록 코드·잘못된 가격은 필드 오류로 표시하고 PUT하지 않는다', async () => {
|
||||||
|
for (const overrides of [
|
||||||
|
{ sizeCode: '' }, { sizeCode: 'XLARGE' }, { sizeCode: 'SMALL' },
|
||||||
|
{ basePriceCan: '' }, { salePriceCan: '' }, { basePriceCan: 0 }, { salePriceCan: 1.5 }, { salePriceCan: 12001 }
|
||||||
|
]) {
|
||||||
|
const { vm, confirmations } = page()
|
||||||
|
let calls = 0
|
||||||
|
Vue.axios = {
|
||||||
|
get: async () => ({ data: envelope([size()]) }),
|
||||||
|
put: async () => { calls++ }
|
||||||
|
}
|
||||||
|
await vm.getSizes()
|
||||||
|
vm.openCreate()
|
||||||
|
Object.assign(vm.form, size({ sizeCode: 'MEDIUM', ...overrides }))
|
||||||
|
await vm.saveSize()
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(confirmations.length, 0)
|
||||||
|
const labels = { sizeCode: '사이즈 코드', basePriceCan: '기본가', salePriceCan: '판매가' }
|
||||||
|
const label = labels[Object.keys(overrides)[0]]
|
||||||
|
const field = nodes(vm._render()).find(node => ['v-text-field', 'v-select'].includes(node.tag) && node.data.attrs.label === label)
|
||||||
|
assert.ok(field.data.attrs['error-messages'])
|
||||||
|
if (overrides.salePriceCan === 12001) assert.equal(field.data.attrs['error-messages'], '판매가는 기본가보다 클 수 없습니다.')
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 최초 등록: 취소·수정 후 다시 열면 등록 폼과 오류를 초기화한다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
Vue.axios = { get: async () => ({ data: envelope([size()]) }) }
|
||||||
|
await vm.getSizes()
|
||||||
|
vm.openCreate()
|
||||||
|
Object.assign(vm.form, size({ sizeCode: 'MEDIUM' }))
|
||||||
|
vm.form_errors = { sizeCode: '오류', salePriceCan: '오류' }
|
||||||
|
vm.closeForm()
|
||||||
|
vm.openCreate()
|
||||||
|
assert.deepEqual(vm.form, createGiftSizeForm())
|
||||||
|
assert.deepEqual(vm.form_errors, {})
|
||||||
|
vm.closeForm()
|
||||||
|
vm.openEdit(vm.sizes[0])
|
||||||
|
assert.equal(nodes(vm._render()).find(node => node.tag === 'v-select'), undefined)
|
||||||
|
assert.ok(text(vm._render()).includes('선물 사이즈 수정'))
|
||||||
|
vm.closeForm()
|
||||||
|
vm.openCreate()
|
||||||
|
assert.deepEqual(vm.form, createGiftSizeForm())
|
||||||
|
assert.ok(text(vm._render()).includes('선물 사이즈 등록'))
|
||||||
|
assert.deepEqual(Array.from(vm.sizes), [size()])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 최초 등록: 고정 3종을 기존 PUT으로 등록하고 정확한 본문·확인창·토스트·재조회를 제공한다', async () => {
|
||||||
|
const { vm, successes, confirmations } = page()
|
||||||
|
const rows = []
|
||||||
|
const calls = []
|
||||||
|
Vue.axios = { get: async () => ({ data: envelope([]) }) }
|
||||||
|
await vm.getSizes()
|
||||||
|
for (const [index, sizeCode] of giftSizes.entries()) {
|
||||||
|
const pending = deferred()
|
||||||
|
const saved = size({ sizeCode, name: `서버 이름 ${index}`, basePriceCan: 13000, salePriceCan: 11000, isActive: index !== 1 })
|
||||||
|
Vue.axios = {
|
||||||
|
put: async (...args) => { calls.push(['PUT', ...args]); rows.push(saved); return { data: envelope(saved) } },
|
||||||
|
get: async (...args) => { calls.push(['GET', ...args]); return pending.promise }
|
||||||
|
}
|
||||||
|
button(vm, '사이즈 등록').data.on.click()
|
||||||
|
Object.assign(vm.form, { sizeCode, basePriceCan: '13000', salePriceCan: '11000', isActive: saved.isActive, extra: '제외' })
|
||||||
|
const save = vm.saveSize()
|
||||||
|
await Vue.nextTick()
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.equal(vm.is_operating, true)
|
||||||
|
assert.equal(button(vm, '사이즈 등록').data.attrs.disabled, true)
|
||||||
|
await vm.saveSize()
|
||||||
|
pending.resolve({ data: envelope([...rows]) })
|
||||||
|
await save
|
||||||
|
assert.deepEqual(calls.slice(index * 2), [
|
||||||
|
['PUT', `/api/v2/admin/gift-size-prices/${sizeCode}`, request({ basePriceCan: 13000, salePriceCan: 11000, isActive: saved.isActive })],
|
||||||
|
['GET', '/api/v2/admin/gift-size-prices']
|
||||||
|
])
|
||||||
|
assert.equal(confirmations[index].title, '선물 사이즈 등록 확인')
|
||||||
|
assert.ok(confirmations[index].text.includes(sizeCode))
|
||||||
|
assert.deepEqual(confirmations[index].actions, { false: '취소', true: '저장' })
|
||||||
|
assert.equal(successes[index], '선물 사이즈가 등록되었습니다.')
|
||||||
|
assert.deepEqual(Array.from(vm.sizes), rows)
|
||||||
|
assert.ok(text(vm._render()).includes(saved.name))
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
}
|
||||||
|
assert.equal(button(vm, '사이즈 등록'), undefined)
|
||||||
|
assert.equal(calls.length, 6)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 최초 등록: 확인 취소·중복 제출을 차단하며 저장 중에는 코드와 입력값을 바꿀 수 없다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const pending = deferred()
|
||||||
|
let calls = 0
|
||||||
|
let confirms = 0
|
||||||
|
Vue.axios = { get: async () => ({ data: envelope([]) }), put: async () => { calls++ } }
|
||||||
|
await vm.getSizes()
|
||||||
|
vm.openCreate()
|
||||||
|
Object.assign(vm.form, size())
|
||||||
|
vm.$dialog.confirm = async () => { confirms++; return pending.promise }
|
||||||
|
const save = vm.saveSize()
|
||||||
|
await vm.saveSize()
|
||||||
|
vm.openCreate()
|
||||||
|
vm.openEdit(size({ sizeCode: 'LARGE' }))
|
||||||
|
vm.closeForm()
|
||||||
|
assert.deepEqual(vm.form, size())
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.equal(confirms, 1)
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
const rendered = nodes(vm._render())
|
||||||
|
for (const field of rendered.filter(node => ['v-select', 'v-text-field', 'v-switch'].includes(node.tag))) {
|
||||||
|
assert.equal(field.data.attrs.disabled, true)
|
||||||
|
}
|
||||||
|
assert.equal(button(vm, '저장').data.attrs.disabled, true)
|
||||||
|
assert.equal(button(vm, '취소').data.attrs.disabled, true)
|
||||||
|
pending.resolve(false)
|
||||||
|
await save
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 최초 등록: 폼을 연 뒤나 확인 중 목록이 변경되면 기존 데이터 덮어쓰기를 차단한다', async () => {
|
||||||
|
for (const timing of ['before', 'confirm']) {
|
||||||
|
for (const state of ['registered', 'loading', 'failure']) {
|
||||||
|
const { vm } = page()
|
||||||
|
const confirmation = deferred()
|
||||||
|
const pending = deferred()
|
||||||
|
let calls = 0
|
||||||
|
Vue.axios = { get: async () => ({ data: envelope([]) }), put: async () => { calls++ } }
|
||||||
|
await vm.getSizes()
|
||||||
|
vm.openCreate()
|
||||||
|
Object.assign(vm.form, size())
|
||||||
|
vm.$dialog.confirm = async () => confirmation.promise
|
||||||
|
const save = timing === 'confirm' ? vm.saveSize() : null
|
||||||
|
Vue.axios.get = state === 'loading'
|
||||||
|
? async () => pending.promise
|
||||||
|
: async () => ({ data: state === 'failure' ? { success: false, message: '조회 실패' } : envelope([size()]) })
|
||||||
|
const load = vm.getSizes()
|
||||||
|
if (state !== 'loading') await load
|
||||||
|
confirmation.resolve(true)
|
||||||
|
if (timing === 'confirm') await save
|
||||||
|
else await vm.saveSize()
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
if (timing === 'confirm' || state === 'registered') assert.ok(vm.form_errors.sizeCode)
|
||||||
|
if (state === 'loading') {
|
||||||
|
pending.resolve({ data: envelope([]) })
|
||||||
|
await load
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 최초 등록 실패: 서버 오류 시 입력을 유지하고 재조회 실패 시 중복 등록 진입을 차단한다', async () => {
|
||||||
|
for (const fail of [
|
||||||
|
async () => ({ data: { success: false, message: '등록 실패' } }),
|
||||||
|
async () => { throw { response: { data: { message: '등록 실패' } } } }
|
||||||
|
]) {
|
||||||
|
const { vm, errors, successes } = page()
|
||||||
|
let loads = 0
|
||||||
|
Vue.axios = { get: async () => { loads++; return { data: envelope([]) } }, put: fail }
|
||||||
|
await vm.getSizes()
|
||||||
|
vm.openCreate()
|
||||||
|
Object.assign(vm.form, size())
|
||||||
|
await vm.saveSize()
|
||||||
|
assert.deepEqual(errors, ['등록 실패'])
|
||||||
|
assert.deepEqual(successes, [])
|
||||||
|
assert.deepEqual(vm.form, size())
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
assert.equal(loads, 1)
|
||||||
|
}
|
||||||
|
const { vm, errors, successes } = page()
|
||||||
|
Vue.axios = { get: async () => ({ data: envelope([]) }), put: async () => ({ data: envelope(size()) }) }
|
||||||
|
await vm.getSizes()
|
||||||
|
vm.openCreate()
|
||||||
|
Object.assign(vm.form, size())
|
||||||
|
Vue.axios.get = async () => { throw new Error('조회 실패') }
|
||||||
|
await vm.saveSize()
|
||||||
|
assert.deepEqual(successes, ['선물 사이즈가 등록되었습니다.'])
|
||||||
|
assert.deepEqual(errors, ['조회 실패'])
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
assert.equal(button(vm, '사이즈 등록').data.attrs.disabled, true)
|
||||||
|
vm.openCreate()
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
Vue.axios.get = async () => ({ data: envelope([size()]) })
|
||||||
|
await button(vm, '다시 조회').data.on.click()
|
||||||
|
vm.openCreate()
|
||||||
|
const select = nodes(vm._render()).find(node => node.tag === 'v-select')
|
||||||
|
assert.deepEqual(Array.from(select.data.attrs.items), ['MEDIUM', 'LARGE'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 폼: 코드와 이름은 읽기 전용, 가격은 number, 취소는 row를 변경하지 않는다', () => {
|
||||||
|
const { vm } = page()
|
||||||
|
const item = size()
|
||||||
|
vm.openEdit(item)
|
||||||
|
const fields = nodes(vm._render()).filter(node => node.tag === 'v-text-field')
|
||||||
|
assert.equal(fields.length, 4)
|
||||||
|
for (const field of fields) {
|
||||||
|
const attrs = field.data.attrs
|
||||||
|
if (['사이즈 코드', '사이즈명'].includes(attrs.label)) assert.ok(attrs.readonly !== undefined && attrs.readonly !== false)
|
||||||
|
else {
|
||||||
|
assert.equal(attrs.type, 'number')
|
||||||
|
assert.equal(Number(attrs.min), 1)
|
||||||
|
assert.equal(Number(attrs.step), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.equal(nodes(vm._render()).filter(node => node.tag === 'v-switch').length, 1)
|
||||||
|
vm.form.basePriceCan = 123
|
||||||
|
vm.form.isActive = false
|
||||||
|
vm.closeForm()
|
||||||
|
assert.deepEqual(item, size())
|
||||||
|
vm.form_errors = { salePriceCan: '오류' }
|
||||||
|
vm.openEdit(size({ sizeCode: 'MEDIUM', basePriceCan: 20000 }))
|
||||||
|
assert.equal(vm.form.sizeCode, 'MEDIUM')
|
||||||
|
assert.equal(vm.form.basePriceCan, 20000)
|
||||||
|
assert.deepEqual(vm.form_errors, {})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 폼: 잘못된 가격이면 API/확인창 없이 해당 필드 오류를 표시한다', async () => {
|
||||||
|
for (const overrides of [{ salePriceCan: 12001 }, { basePriceCan: '' }, { salePriceCan: '' }, { basePriceCan: 0 }, { salePriceCan: 1.5 }]) {
|
||||||
|
const { vm, confirmations } = page()
|
||||||
|
let calls = 0
|
||||||
|
Vue.axios = { put: async () => { calls++ } }
|
||||||
|
vm.openEdit(size(overrides))
|
||||||
|
const form = nodes(vm._render()).find(node => node.tag === 'v-form')
|
||||||
|
await form.data.on.submit({ preventDefault() {} })
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(confirmations.length, 0)
|
||||||
|
const label = Object.keys(overrides)[0] === 'basePriceCan' ? '기본가' : '판매가'
|
||||||
|
const field = nodes(vm._render()).find(node => node.tag === 'v-text-field' && node.data.attrs.label === label)
|
||||||
|
assert.ok(field.data.attrs['error-messages'])
|
||||||
|
if (overrides.salePriceCan === 12001) assert.equal(field.data.attrs['error-messages'], '판매가는 기본가보다 클 수 없습니다.')
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 저장: 확인 전/취소 후에는 PUT하지 않고 중복 제출과 폼 변경을 차단한다', async () => {
|
||||||
|
const { vm } = page()
|
||||||
|
let calls = 0
|
||||||
|
const pending = deferred()
|
||||||
|
let confirms = 0
|
||||||
|
vm.$dialog.confirm = async () => { confirms++; return pending.promise }
|
||||||
|
Vue.axios = { put: async () => { calls++ } }
|
||||||
|
vm.openEdit(size())
|
||||||
|
const save = vm.saveSize()
|
||||||
|
await vm.saveSize()
|
||||||
|
vm.closeForm()
|
||||||
|
vm.openEdit(size({ sizeCode: 'LARGE' }))
|
||||||
|
assert.equal(confirms, 1)
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(vm.form.sizeCode, 'SMALL')
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.equal(button(vm, '저장').data.attrs.disabled, true)
|
||||||
|
assert.equal(button(vm, '취소').data.attrs.disabled, true)
|
||||||
|
pending.resolve(false)
|
||||||
|
await save
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 저장: PUT의 정확한 본문, 성공 토스트, 목록 refresh 후 모달 닫기를 보장한다', async () => {
|
||||||
|
for (const sizeCode of giftSizes) {
|
||||||
|
const { vm, successes, confirmations } = page()
|
||||||
|
const pending = deferred()
|
||||||
|
const calls = []
|
||||||
|
const saved = size({ sizeCode, basePriceCan: 13000, salePriceCan: 11000, isActive: false })
|
||||||
|
Vue.axios = {
|
||||||
|
put: async (...args) => { calls.push(['PUT', ...args]); return { data: envelope(saved) } },
|
||||||
|
get: async (...args) => { calls.push(['GET', ...args]); return pending.promise }
|
||||||
|
}
|
||||||
|
vm.openEdit(size({ sizeCode }))
|
||||||
|
vm.form.basePriceCan = '13000'
|
||||||
|
vm.form.salePriceCan = '11000'
|
||||||
|
vm.form.isActive = false
|
||||||
|
vm.form.extra = '제외'
|
||||||
|
const save = vm.saveSize()
|
||||||
|
await Vue.nextTick()
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.equal(vm.is_operating, true)
|
||||||
|
await vm.saveSize()
|
||||||
|
pending.resolve({ data: envelope([saved]) })
|
||||||
|
await save
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['PUT', `/api/v2/admin/gift-size-prices/${sizeCode}`, request({ basePriceCan: 13000, salePriceCan: 11000, isActive: false })],
|
||||||
|
['GET', '/api/v2/admin/gift-size-prices']
|
||||||
|
])
|
||||||
|
assert.equal(confirmations.length, 1)
|
||||||
|
assert.deepEqual(confirmations[0].actions, { false: '취소', true: '저장' })
|
||||||
|
assert.deepEqual(successes, ['선물 사이즈가 수정되었습니다.'])
|
||||||
|
assert.deepEqual(Array.from(vm.sizes), [saved])
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
await vm.saveSize()
|
||||||
|
assert.equal(calls.length, 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 저장 실패: envelope/HTTP 오류를 알리고 입력값과 모달을 유지한다', async () => {
|
||||||
|
for (const fail of [
|
||||||
|
async () => ({ data: { success: false, message: '저장 실패' } }),
|
||||||
|
async () => { throw { response: { data: { message: '저장 실패' } } } },
|
||||||
|
async () => { throw new Error('저장 실패') }
|
||||||
|
]) {
|
||||||
|
const { vm, errors, successes } = page()
|
||||||
|
let loads = 0
|
||||||
|
Vue.axios = { put: fail, get: async () => { loads++ } }
|
||||||
|
vm.openEdit(size())
|
||||||
|
await vm.saveSize()
|
||||||
|
assert.deepEqual(errors, ['저장 실패'])
|
||||||
|
assert.deepEqual(successes, [])
|
||||||
|
assert.deepEqual(vm.form, size())
|
||||||
|
assert.equal(vm.show_form_dialog, true)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
assert.equal(loads, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 저장 후 조회 실패: 저장 성공과 조회 실패를 구분하고 재시도를 제공한다', async () => {
|
||||||
|
const { vm, errors, successes } = page()
|
||||||
|
Vue.axios = {
|
||||||
|
put: async () => ({ data: envelope(size()) }),
|
||||||
|
get: async () => { throw new Error('조회 실패') }
|
||||||
|
}
|
||||||
|
vm.openEdit(size())
|
||||||
|
await vm.saveSize()
|
||||||
|
assert.deepEqual(successes, ['선물 사이즈가 수정되었습니다.'])
|
||||||
|
assert.deepEqual(errors, ['조회 실패'])
|
||||||
|
assert.equal(vm.show_form_dialog, false)
|
||||||
|
assert.equal(vm.list_error, true)
|
||||||
|
assert.equal(vm.is_operating, false)
|
||||||
|
assert.ok(button(vm, '다시 조회'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('사이즈 메뉴/라우트: ADMIN만 보완하고 기존 서버 항목을 중복 없이 유지한다', async () => {
|
||||||
|
for (const role of ['ADMIN', 'CONTENT_MANAGER']) {
|
||||||
|
for (const existing of ['none', 'null', 'list', 'size', 'object']) {
|
||||||
|
const menus = []
|
||||||
|
if (existing !== 'none') {
|
||||||
|
const item = { title: existing === 'list' ? '선물함 리스트' : '선물 사이즈', route: existing === 'list' ? '/gift/list' : '/gift/size' }
|
||||||
|
menus.push({ title: '선물함 관리', items: existing === 'null' ? null : existing === 'object' ? item : [item] })
|
||||||
|
}
|
||||||
|
Vue.axios = { get: async () => ({ status: 200, data: envelope(menus) }) }
|
||||||
|
const vm = new Vue({ ...SideMenu, created: undefined })
|
||||||
|
vm.$store = { state: { accountStore: { role } } }
|
||||||
|
await vm.getMenus()
|
||||||
|
const gifts = vm.items.filter(item => item.title === '선물함 관리')
|
||||||
|
const children = gifts.length && gifts[0].items ? [].concat(gifts[0].items) : []
|
||||||
|
assert.equal(children.filter(item => item.route === '/gift/size').length, role === 'ADMIN' || ['size', 'object'].includes(existing) ? 1 : 0)
|
||||||
|
if (role === 'ADMIN') {
|
||||||
|
assert.equal(gifts.length, 1)
|
||||||
|
assert.equal(children.filter(item => item.route === '/gift/list').length, 1)
|
||||||
|
assert.equal(children.filter(item => item.route === '/gift/category').length, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const router = fs.readFileSync(path.join(root, 'src/router/index.js'), 'utf8')
|
||||||
|
assert.match(router, /path: '\/gift\/size',\s*name: 'GiftSize',\s*component: \(\) => import\([^\n]*'\.\.\/views\/Gift\/GiftSize.vue'\)/)
|
||||||
|
})
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
for (const { name, run } of tests) {
|
||||||
|
try {
|
||||||
|
await run()
|
||||||
|
console.log(`PASS ${name}`)
|
||||||
|
} catch (error) {
|
||||||
|
process.exitCode = 1
|
||||||
|
console.error(`FAIL ${name}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
Reference in New Issue
Block a user