서비스관리 / 알림톡 템플릿관리 추가

This commit is contained in:
kimre
2022-06-20 13:50:46 +09:00
parent 1d46263735
commit 460d6e03b8
147 changed files with 10360 additions and 6618 deletions

View File

@@ -1,45 +0,0 @@
<template>
<div class="container template_free">
<article id="content" class="content"></article>
<div tabindex="0" class="layer active">
<div tabindex="0" class="layer_cont error">
<div class="layer_body">
<div class="title_wrap center mar_b50">
<h5 class="h5_title" v-html="message">{{ message }}</h5>
</div>
<div class="btn_wrap mar_t20 center">
<a v-for="(branch, index) in branchList" :key="index" href="javascript:void(0);" class="btn mid" :class="branch.class" @click="branch.callback"><span>{{ branch.text }}</span></a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
/*
branchList: [
{
text: "저장",
class: "bd_black",
callback: callback
},
.....
]
*/
export default {
name: "buttonBranch",
props: {
message: String,
branchList: {
text: Array,
class: String,
callback: Function
}
},
data() {
return {
}
}
}
</script>

View File

@@ -1,199 +0,0 @@
<template>
<section>
<!-- <div class="emulator_wrap"> -->
<div class="inner_emul">
<strong class="blind">미리보기</strong>
<div class="emulator_area">
<div class="emulator_cont">
<div class="img_area description">
<img :src="bgImageData" v-if="bgImageData && !retrivebgFlag">
<img :src="this.bgImageUrl" v-else-if="retrivebgFlag">
<img src="@/assets/images/common/img_placeholder02.png" v-else>
</div>
<div class="rcs_profile_area">
<img :src="profileImageData" v-if="profileImageData && !retriveProfileFlag">
<img :src="this.profileImageUrl" v-else-if="retriveProfileFlag">
<img src="@/assets/images/common/img_profile_blank.png" v-else>
</div>
<strong class="rcs_brand_name">{{this.brandInfoData.name}}</strong>
<div class="rcs_icon_area">
<span
v-for="(item, index) in visibleMenuItemList"
:key="index"
class="rcs_icon"
:class="`icon_${item.code.toLowerCase()}`"
></span>
</div>
<div class="rcs_desc_area" v-html="this.brandInfoData.descr"></div>
<div class="rcs_detail_area">
<dl>
<dt>전화번호</dt>
<dd>{{this.brandInfoData.tel}}</dd>
<dt>웹사이트</dt>
<dd>{{this.brandInfoData.url}}</dd>
<dt>이메일</dt>
<dd v-if="this.brandInfoData.email === '@'"></dd>
<dd v-else>{{this.brandInfoData.email}}</dd>
<dt>주소</dt>
<dd>{{this.brandInfoData.addrRegnNo}}{{this.brandInfoData.addrMngNo}}{{this.brandInfoData.addrDtl}}</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- </div> -->
</section>
</template>
<script>
// 스크립트를 정의하는 부분
// 개발자 작업 영역
//import { getImageUrl } from '@/service/code'
// [ECMA6] export default 된 부분이 외부에서 import로 사용할 수 있게 된다.
export default {
// .vue 내부에서 사용되는 model
// model 기반으로 vue는 동작된다.
props: {
brandInfoData: {
type: Object
}
},
data() {
return {
bgImageData: '',
profileImageData: '',
profileImageUrl: '',
bgImageUrl: '',
retriveProfileFlag: false,
retrivebgFlag: false
}
},
created() {
// DOM이 만들어 지기 전 실행 되는 곳
// Data를 사전에 준비할 경우 사용된다.
},
mounted() {
// DOM에 해당 .vue가 들어가게 되면 실행 되는 곳
// onload 상태와 동일하다. DOM 이후에 조작할 경우 이곳에서 수행
},
watch: {
'brandInfoData.descr'() {
this.brandInfoData.descr = this.brandInfoData.descr.replace(/\(|\)|on.*\(|eval\(|javascript/gi,'')
.split('\n')
.join('<br />')
},
'brandInfoData.profileImgFile'() {
if (this.brandInfoData.profileImgFile) {
let reader = new FileReader()
let vm = this
let file = this.brandInfoData.profileImgFile
reader.onload = e => {
vm.profileImageData = e.target.result
}
reader.readAsDataURL(file)
} else {
this.profileImageData = ''
}
},
'brandInfoData.bgImgFile'() {
if (this.brandInfoData.bgImgFile) {
let reader = new FileReader()
let vm = this
let file = this.brandInfoData.bgImgFile
reader.onload = e => {
vm.bgImageData = e.target.result
}
reader.readAsDataURL(file)
} else {
this.bgImageData = ''
}
},
'brandInfoData.profileImgFileId'() {
if (
!jglib.isEmpty(this.brandInfoData.profileImgFileId) &&
!jglib.isEmpty(this.brandInfoData.profileImgFileNo)
) {
this.retriveProfileFlag = true
let reqObj = {
fileId: this.brandInfoData.profileImgFileId,
fileNo: this.brandInfoData.profileImgFileNo
}
getImageUrl(reqObj).then(res => {
this.profileImageUrl = res.downloadUrl
})
} else {
this.retriveProfileFlag = false
this.profileImageUrl = ''
}
},
'brandInfoData.bgImgFileId'() {
if (
!jglib.isEmpty(this.brandInfoData.bgImgFileId) &&
!jglib.isEmpty(this.brandInfoData.bgImgFileNo)
) {
this.retrivebgFlag = true
let reqObj = {
fileId: this.brandInfoData.bgImgFileId,
fileNo: this.brandInfoData.bgImgFileNo
}
getImageUrl(reqObj).then(res => {
this.bgImageUrl = res.downloadUrl
})
} else {
this.retrivebgFlag = false
this.bgImageUrl = ''
}
}
},
computed: {
// 값이 자주 변경됨에 따라, 관련되어 데이터 혹은 view가 바뀌어야 할 경우 사용
// method를 바로 연결하면 tic 단위로 계속 실행되기 때문에, 값이 변경할 때만 수행되고,
// cache로 남는 computed를 활용하는 것이 성능에 좋음
visibleMenuItemList() {
if (this.brandInfoData.menuItemList) {
return this.brandInfoData.menuItemList.filter(res => {
return res.visible
})
} else {
return []
}
}
},
methods: {
// .vue 내부에서 사용되는 함수를 정의한다.
// 이벤트에 따라 실행하거나, 내부적으로 사용되는 함수들을 정의한다.
getImageUrl: function(reqData) {
if (!isUseAPI()) {
return new Promise((resolve, reject) => {
let res = {
code: '99999999',
msg: 'not available in mockup'
}
resolve(res)
})
}
return new Promise((resolve, reject) => {
request({
url: `/file/${reqData.fileId}/${reqData.fileNo}`,
method: 'get'
})
.then(res => {
let imgData = {
fileName: res.result.fileName,
downloadUrl: res.result.downloadUrl
}
resolve(imgData)
})
.catch(res => {
reject('error in filedownload')
})
})
}
}
}
</script>

View File

@@ -1,235 +0,0 @@
<template>
<div
:class="kind ? 'block' : 'hidden'"
:style="'z-index: ${zIndex};'"
v-if="visible"
class="layer mid active"
>
<div class="layer_cont">
<div class="layer_head">
<h2>{{title}}</h2>
<slot name="header"></slot>
</div>
<div class="layer_body">
<slot>
<p>{{content}}</p>
</slot>
<!--scroll style="max-height:50vh; position: relative;" :settings="scrollSettings">
</scroll-->
</div>
<div class="layer_foot">
<div class="check_wrap" v-if="checkLabel">
<span class="custom_checkbox">
<input type="checkbox" id="checkbox01" v-model="checked">
<label for="checkbox01">{{checkLabel}}</label>
</span>
</div>
<slot name="footer">
<div class="btn_wrap center" v-if="yesBtn" style="margin-bottom: 20px;">
<a href="javascript:void(0)" @click="handleWrapperClick(kind, 'yes')" class="btn mid point">
<span>{{yesBtn}}</span>
</a>
</div>
<div class="btn_wrap center" v-if="noBtn" style="margin-bottom: 20px;">
<a href="javascript:void(0)" @click="handleWrapperClick(kind)" class="btn mid point">
<span>{{noBtn}}</span>
</a>
</div>
<!-- <a href="javascript:void(0)" @click="handleWrapperClick('close')" class="btn_close">
</a> -->
</slot>
</div>
</div>
</div>
</template>
<script>
import Scroll from 'vue-custom-scrollbar'
export default {
name: 'AddressCustomPopup',
components: {
Scroll
},
props: {
visible: {
type: Boolean,
require: true,
default: false
},
title: {
type: String,
require: true
},
checkLabel: {
type: String,
require: true
},
searchProp: {
type: Boolean,
require: true,
default: false
},
kind: {
type: String,
require: true
},
content: {
type: String,
require: false
},
widths: {
type: Object,
require: false,
default() {
return {
widthLg: 'w-1/2',
widthXl: 'w-2/5'
}
}
},
zIndex: {
type: Number,
require: false,
default() {
return 99999
}
},
marginTop: {
type: String,
require: false,
default() {
return '40' // px 단위
}
},
maxHeight: {
type: String,
require: false,
default() {
return '80' // px 단위
}
},
boxPX: {
type: String,
require: false,
default() {
return 'px-4'
}
},
boxPY: {
type: String,
require: false,
default() {
return 'py-8'
}
},
uiframeBox: {
type: Boolean,
require: false,
default: true
},
yesBtn: {
type: String,
require: false
},
noBtn: {
type: String,
require: false
},
close: Function,
inBody: {
type: Boolean,
require: false,
default: false
},
visibleScroll: {
type: Boolean,
default: true
}
},
data() {
return {
contents: false,
checked: true,
scrollSettings: {
suppressScrollY: false
}
}
},
created() {
this.scrollOps();
console.log("AddressCustomPoipup >", this.visible);
},
mounted() {
if (this.inBody) {
document.body.appendChild(this.$el)
}
},
watch: {
visibleScroll() {
this.scrollOps()
}
},
methods: {
handleWrapperClick(id, callback) {
this.$emit('close', {
id: id,
ok: callback === 'yes'
})
},
scrollOps() {
this.scrollSettings.suppressScrollY = !this.visibleScroll
}
}
}
</script>
<style lang="less" scoped>
.block {
animation: fadein 0.5s;
-webkit-animation: fadein 0.5s;
-ms-animation: fadein 0.5s;
}
.height_60 {
height: 60px;
padding-top: 10px;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
margin: 0 1rem;
width: calc(100% - 2rem);
height: 1px;
background: #eff3f6;
}
}
.content_h {
height: calc(100% - 50px);
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-webkit-keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-ms-keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>

View File

@@ -1,332 +0,0 @@
<template>
<!-- 커스텀팝업 -->
<address-custom-popup
:visible="visible"
@close="closePopup"
kind="custom"
title="우편번호 찾기"
:noBtn="view.noBtnText"
:yesBtn="view.yesBtnText"
:zIndex="500"
boxPY="pb-5"
:visibleScroll="view.visibleScroll"
>
<template v-slot:header>
</template>
<div class="box_search_wrap" v-if="view.visibleSearch">
<div class="box_search_area">
<div class="filter_bundle_wrap">
<div class="filter_bundle">
<div class="cont_bundle full_width">
<span class="custom_input col_10" style="width: 80%">
<input type="text" placeholder="도로명 또는 건물명(아파트)을 입력해주세요"
ref="addressNm"
v-model="addressNm"
@keypress.enter="search">
</span>
<a href="javascript:void(0);" class="btn mid gray square col_2" @click="search"><span>검색</span></a>
</div>
</div>
</div>
</div>
</div>
<div class="mar_t20" v-if="!view.visibleDetails">
<!-- 검색 건수 -->
<p class="mar_t20" v-if="view.visibleTotalcount">
<span class="result_count left full_width">{{ page.totalcount }}</span>건이 검색되었습니다.
</p>
<!-- 디폴트 문구 -->
<div
class="mar_t20"
v-if="isVisibleDescription"
:class="{'line': view.visibleTotalcount}"
>
<p class="txt_17">도로명 주소와 건물번호를 함께 입력하시면 빠르게 결과를 확인하실 있습니다.</p>
<ul class="mar_t10 txt_16 text_gray2">
<li>도로명 + 건물번호 (: 송파대로 570)</li>
<li>도로명( : 강남대로, 중앙1로, 낙산1길)</li>
<li>건물명, 아파트명 (: 반포자이아파트)</li>
</ul>
</div>
<!-- 리스트 -->
<table class="tbl_row_type type3" v-if="!isVisibleDescription">
<colgroup>
<col style="width:100px;">
<col style="width:350px;">
<col style="width:auto;">
</colgroup>
<thead>
<tr>
<th>우편번호</th>
<th scope="row">도로명</th>
<th>지번</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in list" :key="`list-${index}`">
<td><a href="javascript:void(0);" @click="selectAddr(row)"> {{ row.zipCode }}</a></td>
<td><a href="javascript:void(0);" @click="selectAddr(row)"> {{ row.doroAddressNm }}</a></td>
<td><a href="javascript:void(0);" @click="selectAddr(row)"> {{ row.jibunAddressNm }}</a></td>
</tr>
</tbody>
</table>
<!-- 리스트 페이징 -->
<pagination
v-if="!isVisibleDescription"
:total="page.totalcount"
:current-page.sync="page.currentPage"
:pageSize="page.size"
rangeMax="5"
@change="changePage"
class="type2"
></pagination>
</div>
<!-- 상세주소 입력 -->
<div class="search_result" v-if="view.visibleDetails">
<table class="tbl_row_type">
<caption>
<strong>우편번호 찾기</strong>
<p>우편번호, 도로명/지번 주소</p>
</caption>
<colgroup>
<col style="width:120px;">
<col style="width:auto;">
</colgroup>
<tbody>
<tr>
<th scope="row">우편번호</th>
<td>{{ selectedAddr.zipCode }}</td>
</tr>
<tr>
<th scope="row">
도로명
<br>지번
</th>
<td>
<p>{{ selectedAddr.doroAddressNm }}</p>
<p>{{ selectedAddr.jibunAddressNm }}</p>
</td>
</tr>
</tbody>
</table>
</div>
<div class="box_search_area mar_t20" v-if="view.visibleDetails">
<div class="filter_bundle_wrap">
<div class="filter_bundle">
<div class="cont_bundle full_width">
<span class="custom_input full_width" style="width: 100%"><input type="text" placeholder="상세주소를 입력해주세요" v-model="selectedAddr.detail"></span>
</div>
</div>
</div>
<a href="javascript:void(0)" @click="handleWrapperClick" class="btn_close"></a>
</div>
</address-custom-popup>
</template>
<script>
import AddressCustomPopup from './AddressCustomPopup'
import jglib from '@/utils/jglib'
import commApi from '@/common/comm-api'
import Pagination from '@/components/Pagination'
export default {
name: 'AddressPopup',
components: {
AddressCustomPopup,
jglib,
Pagination
},
props: {
visible: {
type: Boolean,
default: true
}
},
data() {
return {
view: {
visibleSearch: true,
visibleTotalcount: false,
visibleDetails: false,
noBtnText: '닫기',
yesBtnText: '',
visibleScroll: false,
pagination: true,
},
addressNm: '',
list: [],
page: {
totalcount: 0,
currentPage: 5,
size: 5
},
selectedAddr: {
doroAddressNm: '',
jibunAddressNm: '',
zipCode: '',
detail: ''
}
}
},
computed: {
isVisibleDescription() {
return parseInt(this.page.totalcount) === 0
}
},
created() {
this.toggleButton(true);
},
mounted() {},
watch: {
'view.visibleDetails'(value) {
if (value) {
this.setVisibleScroll(false)
}
},
isVisibleDescription(value) {
if (!value) {
this.$nextTick(() => {
this.setVisibleScroll(true)
})
}
}
},
methods: {
search() {
// validate
if (jglib.isEmpty(this.addressNm)) {
alert(
'도로명 또는 건물명(아파트)을 입력해주세요'
).then(() => {
this.$refs.addressNm.focus()
})
return false
}
this.view.visibleDetails = false
this.page.currentPage = 1
this.retrieveAddress()
},
retrieveAddress() {
let params = {
page: this.page.currentPage,
psize: this.page.size,
addressNm: this.addressNm
}
commApi
.getAddressList(params)
.then(response => {
var rsp = response.data
if (rsp.success) {
this.list = rsp.data
this.page.totalcount = rsp.search.totalCount
this.view.visibleTotalcount = true
if (this.page.totalcount === 0) {
this.view.visibleDescription = true
} else {
this.view.visibleDescription = false
}
}
})
.catch(err => {
alert(err.desc)
})
},
selectAddr(addr) {
Object.assign(this.selectedAddr, addr)
this.view.visibleSearch = false
this.view.visibleDetails = true
this.toggleButton(false)
},
toggleButton(isClose) {
if (isClose) {
// 닫기 버튼
this.view.noBtnText = '닫기'
this.view.yesBtnText = ''
} else {
// 확인
this.view.noBtnText = ''
this.view.yesBtnText = '확인'
}
},
closePopup(res) {
if(res.id == 'close'){
this.$emit('closeAddressPopup', false);
} else {
if (this.view.visibleDetails && jglib.isEmpty(this.selectedAddr.detail)) {
alert('상세주소를 입력해 주세요.');
} else {
if (res.ok) {
let data = {
zipNo: this.selectedAddr.zipCode,
roadFullAddr: this.selectedAddr.doroAddressNm,
detail: this.selectedAddr.detail
}
this.$emit('getData', data)
} else {
this.$emit('closeAddressPopup', false);
}
}
}
},
changePage(page) {
this.page.currentPage = page
this.retrieveAddress()
},
setVisibleScroll(flag) {
this.view.visibleScroll = flag
},
handleWrapperClick() {
this.$emit('closeAddressPopup', false);
},
}
}
</script>
<style lang="less" scoped>
.layer .search_result .address_list {
border-top: 2px solid #666;
border-bottom: 1px solid #aaa;
letter-spacing: -0.02em;
}
.layer .search_result .address_list li + li {
border-top: 1px solid #ccc;
}
.layer .search_result .address_list li a {
display: flex;
align-items: center;
}
.layer .search_result .address_list li a:hover {
color: #f5f5f5;
}
.layer .search_result .address_list li a > .address {
width: ~'calc(100% - 70px)';
padding: 15px 10px 15px 20px;
box-sizing: border-box;
}
.layer .search_result .address_list li a > .zipcode {
width: 70px;
padding: 20px 20px 20px 0;
box-sizing: border-box;
text-align: right;
}
.layer .search_result .address_list li p {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
line-height: 1.5;
}
.layer .search_result .address_list li p strong.title {
width: 45px;
margin-right: 20px;
}
.layer .search_result .address_list li p span {
width: ~'calc(100% - 65px)';
font-size: 16px;
color: #666;
font-weight: normal;
font-family: 'NotoSansLight';
}
</style>