This commit is contained in:
2026-04-07 14:50:23 +09:00
commit b4e485502b
4778 changed files with 2017091 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
<template>
<div
v-if="
(attachments.length > 0 && readOnlyFlag == true) ||
readOnlyFlag == false
"
class="mt-3 sm:col-span-2"
>
<dt class="text-sm font-medium text-gray-500">
<p v-if="readOnlyFlag" class="font-medium">첨부된 파일</p>
<a
v-else
href="javascript:void(0)"
class="font-medium text-blue-600 hover:text-blue-500"
@click="addFiles()"
>
파일 첨부
</a>
</dt>
<dd class="mt-1 text-sm text-gray-900">
<ul
role="list"
class="border border-gray-200 rounded-md divide-y divide-gray-200"
>
<li
v-for="attachment in attachments"
:key="attachment.name"
class="pl-3 pr-4 py-3 flex items-center justify-between text-sm"
>
<div class="w-0 flex-1 flex items-center">
<PaperClipIcon
class="flex-shrink-0 h-5 w-5 text-gray-400"
aria-hidden="true"
/>
<span class="ml-2 flex-1 w-0 truncate">
{{ attachment.name }} {{ ', ' }}
{{ _utils.formatBytes(attachment.size, 2) }}
</span>
</div>
<div v-if="readOnlyFlag" class="ml-4 flex-shrink-0">
<a
:href="
_crossCtl.config['API_BASE_URL'].replace(
'/api/',
''
) + attachment.localUrl
"
:download="attachment.name"
target="_blank"
class="font-medium text-blue-600 hover:text-blue-500"
>
다운로드
</a>
</div>
<div v-else class="ml-4 flex-shrink-0">
<a
href="javascript:void(0)"
class="font-medium text-red-600 hover:text-red-500"
@click="rmvFile(attachment)"
>
삭제
</a>
</div>
</li>
</ul>
</dd>
</div>
</template>
<script setup lang="ts">
import { stringLiteral } from '@babel/types';
import { PaperClipIcon } from '@heroicons/vue/24/solid';
const props = defineProps({
attachments: {
type: Array<{ name: ''; localUrl: ''; size: 0; type: 'text/html' }>,
default: [],
},
readOnlyFlag: { type: Boolean, required: true },
updateAttachments: {
type: Function,
required: false,
default: () => {
void 0;
},
},
boardId: { type: String, required: false, default: null },
secureEnabled: { type: Boolean, required: false, default: false },
});
// console.log('huk props = ', props);
const currentDomain = ref('');
if (process.client) {
currentDomain.value = _utils.getDomain(window.location.href);
console.log('currentDomain.value=', currentDomain.value);
} else {
console.log('server?');
}
function addFiles() {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('multiple', 'multiple');
input.click();
// Listen upload local image and save to server
input.onchange = () => {
if (input.files.length > 0) {
console.log('we got file(s) : ', input.files);
uploadFiles(input.files);
}
};
}
async function uploadFiles(files) {
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('upload-file', files[i], files[i].name);
}
formData.append('target', 'just');
if (props.boardId != null) {
formData.append('attachedTo', props.boardId);
}
formData.append('secureEnabled', props.secureEnabled.toString());
console.log('formData=', formData);
const responseJson = await _crossCtl.doUpload('just', formData);
console.log('responseJson=', responseJson);
if (responseJson['responseCode'] == 200) {
props.updateAttachments([
...props.attachments,
...responseJson['files'],
]);
} else {
alert('upload error : ' + responseJson['responseMessage']);
}
}
function rmvFile(target) {
const tmpAry = [];
for (let i = 0; i < props.attachments.length; i++) {
if (props.attachments[i] != target) {
tmpAry.push(props.attachments[i]);
}
}
props.updateAttachments(tmpAry);
}
</script>

View File

@@ -0,0 +1,40 @@
<template>
<div>
<span
v-if="photoUrl == ''"
class="inline-block rounded-full overflow-hidden bg-gray-100"
:style="'height:' + photoSize + 'rem; width:' + photoSize + 'rem'"
>
<svg
class="h-full w-full text-gray-300"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"
></path>
</svg>
</span>
<img
v-else
class="'inline-block rounded-full border'"
:style="'height:' + photoSize + 'rem; width:' + photoSize + 'rem'"
:src="photoUrl"
:alt="imageAlt"
/>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
imageSize: { type: Number, default: 12 },
imageAlt: { type: String, default: '' },
imageUrl: { type: String, default: '' },
});
const photoSize = ref(props.imageSize);
const photoUrl = ref(props.imageUrl);
const imageAlt = ref(props.imageAlt);
</script>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,139 @@
<template>
<div class="flex flex-col">
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div
class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8"
>
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th
v-for="(heading, index) in headings"
:key="index"
:width="
heading['widthRatio'] != ''
? heading['widthRatio'] + '%'
: '100%'
"
:class="
index == 0
? 'whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 md:pl-0'
: 'whitespace-nowrap py-3.5 px-3 text-left text-sm font-semibold text-gray-900'
"
scope="col"
>
{{ heading['title'] }}
</th>
<th
v-for="(action, index) in actions"
:key="index + headings.length"
:width="actions.length * 1"
class="relative py-3.5 pl-3 pr-4 sm:pr-6 md:pr-0"
scope="col"
>
<span class="sr-only">{{ action }}</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="(item, itemIndex) in data" :key="itemIndex">
<td
v-for="(heading, index) in headings"
:key="index"
:class="
index == 0
? 'max-w-0 whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 md:pl-0'
: 'w-full whitespace-nowrap py-4 px-3 text-sm text-gray-500'
"
>
<div v-if="index == 0" class="">
<a
href="javascript:void(0)"
class="hover:bg-gray-50"
@click="
doAction('보기', item[actionKey])
"
><div class="truncate">
{{
columnFilter
? columnFilter(
heading['key'],
item[heading['key']]
)
: item[heading['key']]
}}
</div>
</a>
</div>
<div v-else>
{{
columnFilter
? columnFilter(
heading['key'],
item[heading['key']]
)
: item[heading['key']]
}}
</div>
</td>
<td
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 md:pr-0"
>
<a
v-for="(action, index) in actions"
:key="index"
href="javascript:void(0)"
:class="index != 0 ? 'ml-3' : ''"
class="text-indigo-600 hover:text-indigo-900"
@click="doAction(action, item[actionKey])"
>{{ action
}}<span class="sr-only"
>, {{ item['serial'] }}</span
></a
>
</td>
</tr>
<tr v-if="data.length == 0">
<td
:colspan="headings.length"
class="whitespace-nowrap py-4 px-3 text-sm text-gray-500"
>
{{ 'No Data to display...' }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const people = [
{
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com 아주 긴 아지 긴 아주 긴 이런 저런 긴 길 저런 길 갈 골 갈',
role: 'Member',
},
// More people...
];
const props = defineProps({
headings: { type: Array<object>, required: true },
actions: { type: Array<string>, default: [] },
data: { type: Array<object>, required: true },
actionKey: { type: String, default: 'serial' },
columnFilter: {
type: Function,
default: (key: string, val: string) => val,
},
doAction: {
type: Function,
default: (key: string) => {
return key;
},
},
});
</script>

View File

@@ -0,0 +1,73 @@
<template>
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div
class="px-4 py-5 sm:px-6 flex justify-between items-center flex-wrap sm:flex-nowrap"
>
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ title }}
</h3>
</div>
<div>
<div class="flex items-start">
<div
class="flex-shrink-0 inline-flex rounded-full border-2 border-white"
>
<BaseAvater1
:image-size="2"
:image-url="profileUrl"
:image-alt="name + ' 프로필 사진'"
/>
</div>
<div class="ml-4">
<div class="text-base font-medium">{{ name }}</div>
<div class="text-base text-xs">{{ created }}</div>
</div>
</div>
</div>
</div>
<div class="border-t border-gray-200 px-4 py-5 sm:px-6 min-h-[15rem]">
<div
ref="myCoolDiv"
class="prose mt-3 space-y-0 max-w-none"
v-html="content"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const props = defineProps({
name: { type: String, required: true },
profileUrl: { type: String, required: true },
title: { type: String, required: true },
content: { type: String, required: true },
flags: { type: Array<string>, default: [] },
attachments: { type: Array<string>, default: [] },
hitCount: { type: Number, required: true },
likeCount: { type: Number, required: true },
dislikeCount: { type: Number, required: true },
commentCount: { type: Number, required: true },
reportCount: { type: Number, required: true },
status: { type: Number, required: true },
updated: { type: String, required: true },
created: { type: String, required: true },
});
const myCoolDiv = ref(null);
watch(myCoolDiv, () => {
if (myCoolDiv.value.childNodes[0].tagName == 'IFRAME') {
myCoolDiv.value.childNodes[0].classList.add('xl:w-[1243px]');
myCoolDiv.value.childNodes[0].classList.add('xl:h-[621.5px]');
myCoolDiv.value.childNodes[0].classList.add('lg:w-[1243px]');
myCoolDiv.value.childNodes[0].classList.add('lg:h-[621.5px]');
myCoolDiv.value.childNodes[0].classList.add('md:w-[900px]');
myCoolDiv.value.childNodes[0].classList.add('md:h-[400px]');
myCoolDiv.value.childNodes[0].classList.add('w-[310px]');
myCoolDiv.value.childNodes[0].classList.add('h-[250px]');
}
});
</script>

View File

@@ -0,0 +1,372 @@
<template>
<!-- Comments-->
<section aria-labelledby="notes-title">
<div
class="border border-gray-300 shadow sm:rounded-lg sm:overflow-hidden"
>
<div class="divide-y divide-gray-200">
<div class="px-4 py-5 sm:px-6">
<h2
id="notes-title"
class="text-lg font-medium text-gray-900"
>
{{ title }}
</h2>
</div>
<div class="px-4 py-6 sm:px-6">
<ul role="list" class="space-y-8">
<li
v-for="commentItem in listData"
:key="commentItem.serial"
>
<div class="flex space-x-3">
<div class="flex-shrink-0 items-center">
<BaseAvater1
:image-size="2"
:image-url="commentItem.profile_url"
/>
</div>
<div>
<div class="text-sm">
<a
:href="
'/user/profile/' +
commentItem.pid
"
class="font-medium text-gray-900"
>{{ commentItem.nick }}</a
>
</div>
<div
class="mt-1 text-sm text-gray-700 break-all"
>
<p>{{ commentItem.comment }}</p>
</div>
<div class="mt-2 text-sm space-x-2">
<span
class="text-gray-500 font-medium"
>{{
$dayjs(
commentItem.created
).fromNow()
}}</span
>
{{ ' ' }}
<span
v-if="!readOnlyFlag"
class="text-gray-500 font-medium"
>&middot;</span
>
{{ ' ' }}
<span
v-if="commentItem.like_count != 0"
class="text-gray-500 font-medium"
>{{
'좋아요 ' +
_utils.formatNumberInBytesStyle(
commentItem.like_count,
0
) +
'개'
}}</span
>
<span
v-if="
commentItem.dislike_count != 0
"
class="text-gray-500 font-medium"
>{{
'싫어요 ' +
_utils.formatNumberInBytesStyle(
commentItem.dislike_count,
0
) +
'개'
}}</span
>
<span
v-if="commentItem.report_count != 0"
class="text-gray-500 font-medium"
>{{
'신고 ' +
_utils.formatNumberInBytesStyle(
commentItem.report_count,
0
) +
'개'
}}</span
>
<button
v-if="
commentItem.myFlag == true &&
!readOnlyFlag
"
type="button"
class="text-gray-900 font-medium"
@click="
doAction(
'delete',
commentItem.cid
)
"
>
삭제
</button>
<button
v-if="!readOnlyFlag"
type="button"
class="text-gray-900 font-medium"
@click="
doAction(
'like',
commentItem.cid
)
"
>
좋아요
</button>
<button
v-if="!readOnlyFlag"
type="button"
class="text-gray-900 font-medium"
@click="
doAction(
'dislike',
commentItem.cid
)
"
>
싫어요
</button>
<button
v-if="!readOnlyFlag"
type="button"
class="text-gray-900 font-medium"
@click="
doAction(
'report',
commentItem.cid
)
"
>
신고
</button>
<button
v-if="!readOnlyFlag"
type="button"
class="text-gray-900 font-medium"
@click="
doAction(
'cancel',
commentItem.cid
)
"
>
신고 취소
</button>
</div>
</div>
</div>
</li>
<li v-if="listData.length == 0">
<div>등록된 댓글이 없습니다.</div>
</li>
</ul>
</div>
<BasePagination1
class="mb-5 px-4 sm:px-6"
:total-page-count="totalPageCount"
:current-page-number="currentPageNumber"
:page-size="pageSize"
:records-total="recordsTotal"
:page-move="pageMove"
/>
</div>
<div v-if="!readOnlyFlag" class="bg-gray-50 px-4 py-6 sm:px-6">
<div class="flex space-x-3">
<div class="flex-shrink-0">
<BaseUserProfileImage :image-size="2" />
</div>
<div class="min-w-0 flex-1">
<form>
<div>
<label for="comment" class="sr-only">{{
title
}}</label>
<textarea
id="comment"
v-model="comment"
style="resize: none"
name="comment"
rows="3"
class="shadow-sm block w-full focus:ring-blue-500 focus:border-blue-500 sm:text-sm border border-gray-300 rounded-md"
placeholder="댓글 내용을 입력하세요."
/>
</div>
<div class="mt-3 flex items-center justify-between">
<a
href="#"
class="group inline-flex items-start text-sm space-x-2 text-gray-500 hover:text-gray-900"
>
<QuestionMarkCircleIcon
class="flex-shrink-0 h-5 w-5 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
<span>
모욕적인 표현이 포함된 댓글은 등록이
거부될 수 있습니다.
</span>
</a>
<button
type="button"
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
@click="addComment()"
>
댓글 등록
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { QuestionMarkCircleIcon } from '@heroicons/vue/24/solid';
const comment = ref('');
const props = defineProps({
tid: { type: String, required: true },
title: { type: String, default: '댓글' },
readOnlyFlag: { type: Boolean, required: true },
});
// console.log('huk props = ', props);
const title = ref(props.title);
console.log('title = ', title.value);
const listData = ref([]);
const readOnlyFlag = ref(props.readOnlyFlag);
const totalPageCount = ref(1);
const currentPageNumber = ref(Number.MAX_SAFE_INTEGER);
const pageSize = ref(3);
const recordsTotal = ref(0);
function pageMove(targetPageIdex) {
// console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function addComment() {
if (comment.value.trim() != '') {
const responseJson = await _crossCtl.doComm('insert', 'comment', {
hero: props.tid,
comment: comment.value,
for: 'board',
});
if (responseJson['responseCode'] != 200) {
alert(responseJson['responseMessage']);
} else {
console.log('responseJson=', responseJson);
comment.value = '';
refresh();
}
}
}
async function refresh() {
if (props.tid != '') {
const responseJson = await _crossCtl.doComm('list', 'comment:active', {
hero: props.tid,
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
});
if (responseJson['responseCode'] != 200) {
alert(responseJson['responseMessage']);
} else {
console.log('responseJson=', responseJson);
currentPageNumber.value = responseJson['currentPageNumber'];
totalPageCount.value = responseJson['totalPageCount'];
pageSize.value = parseInt(responseJson['pageSize']);
recordsTotal.value = responseJson['recordsTotal'];
listData.value = responseJson['data'];
}
}
}
if (props.tid != '' && _crossCtl.isAuthenticated) {
const responseJson = await _crossCtl.doComm('list', 'like', {
hero: props.tid,
start: 0,
length: -1,
});
if (responseJson['responseCode'] != 200) {
alert(responseJson['responseMessage']);
} else {
console.log('like responseJson=', responseJson);
refresh();
}
}
async function doAction(tag, target) {
console.log('in doAction(), tag =', tag, ', target =', target);
let tmpResponseJson = null;
switch (tag) {
case 'like':
case 'dislike':
tmpResponseJson = await _crossCtl.doComm('update', 'like', {
domain: props.tid,
hero: target,
for: 'comment',
tag: tag,
});
refresh();
break;
case 'report':
tmpResponseJson = await _crossCtl.doComm('update', 'report', {
domain: props.tid,
hero: target,
for: 'comment',
tag: tag,
});
refresh();
break;
case 'cancel':
tmpResponseJson = await _crossCtl.doComm('update', 'report', {
domain: props.tid,
hero: target,
for: 'comment',
tag: tag,
});
refresh();
break;
case 'delete':
tmpResponseJson = await _crossCtl.doComm('delete', 'comment', {
hero: target,
tid: props.tid,
from: 'board',
});
refresh();
break;
}
}
</script>

View File

@@ -0,0 +1,25 @@
<template>
<div>
<dt class="text-lg leading-6 font-medium text-gray-900">
{{ item.question }}
</dt>
<dd
class="mt-2 text-base text-gray-500"
style="
white-space: pre-wrap;
word-wrap: break-word;
font-family: inherit;
"
v-html="item.answer.replace(/(?:\r\n|\r|\n)/g, '<br />')"
></dd>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
item: { type: Object, required: true },
});
</script>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,141 @@
<template>
<div class="pb-8 px-0 sm:px-0 lg:px-0">
<div
class="-mx-4 mt-8 overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:-mx-6 md:mx-0 md:rounded-lg"
>
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th
v-for="(heading, index) in headings"
:key="index"
scope="col"
:class="heading['class']"
>
{{ heading['title'] }}
</th>
<th
v-for="(action, index) in actions"
:key="index + headings.length"
scope="col"
class="relative py-3.5 pl-3 pr-4 sm:pr-6"
>
<span class="sr-only">{{ action }}</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<tr v-if="data.length == 0">
<td>
<div class="py-3.5 pl-3 pr-4 sm:pr-6">
{{ noDataMessage }}
</div>
</td>
</tr>
<tr v-for="(item, itemIndex) in data" :key="itemIndex">
<td
v-for="(heading, index) in headings"
:key="index"
:class="heading['subClass']"
>
{{
columnFilter
? columnFilter(
heading['key'],
item[heading['key']]
)
: item[heading['key']]
}}
<dl
v-for="(hItem, hItemIndex) in heading[
'hiddenInfo'
]['dts']"
:key="hItemIndex"
:class="heading['hiddenInfo']['headClass']"
>
<dt
:class="
heading['hiddenInfo']['dts'][
hItemIndex
]['class']
"
>
{{
heading['hiddenInfo']['dts'][
hItemIndex
]['title']
}}
</dt>
<dd
:class="
heading['hiddenInfo']['dds'][
hItemIndex
]['class']
"
>
{{
columnFilter
? columnFilter(
heading['hiddenInfo']['dds'][
hItemIndex
]['key'],
item[
heading['hiddenInfo'][
'dds'
][hItemIndex]['key']
]
)
: item[
heading['hiddenInfo']['dds'][
hItemIndex
]['key']
]
}}
</dd>
</dl>
</td>
<td
class="py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"
>
<a
v-for="(action, index) in actions"
:key="index"
href="javascript:void(0)"
class="text-indigo-600 hover:text-indigo-900"
@click="doAction(action, item[actionKey])"
>{{ action
}}<span class="sr-only"
>, {{ item['serial'] }}</span
></a
>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
headings: { type: Array<object>, required: true },
actions: { type: Array<string>, default: [] },
keys: { type: Array<string>, default: [] },
data: { type: Array<object>, required: true },
actionKey: { type: String, default: 'serial' },
columnFilter: {
type: Function,
default: (key: string, val: string) => val,
},
doAction: {
type: Function,
default: (key: string) => {
return key;
},
},
noDataMessage: { type: String, default: '검색된 데이터가 없습니다.' },
});
</script>

View File

@@ -0,0 +1,125 @@
<template>
<div class="flex flex-col overflow-hidden rounded-lg shadow-lg">
<div
v-if="isLoading"
class="flex flex-1 flex-col justify-between bg-white p-6"
>
<div class="flex-1 relative">
<div class="flex border-b-2 border-indigo-500 pb-2">
<p class="text-sm font-medium text-indigo-600">
<a
href="javascript:void(0)"
@click="pageMove(pageTitle[0].id, '')"
>{{
pageTitle[0]?.title ?? pageTitle[0]?.title
}}&nbsp;</a
>
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6 text-indigo-500 absolute right-0 cursor-pointer"
@click="pageMove(pageTitle[0].id, '')"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6v12m6-6H6"
/>
</svg>
</div>
<a
v-for="list in dataList"
:key="list.serial"
class="mt-2 block"
>
<p
class="mt-3 text-base text-gray-500 cursor-pointer"
@click="pageMove(pageTitle[0].id, list)"
>
{{
list.title.length >= 20
? list.title.substr(0, 20) + '...'
: list.title.substr(0, 20)
}}
</p>
</a>
</div>
</div>
<div v-else class="flex flex-1 flex-col justify-between bg-white p-6">
<div class="flex-1 relative">
<div class="flex pb-2 bg-sky-100 animate-pulse rounded-lg">
<p class="text-sm font-medium text-gray-600">
<a href="javascript:void(0)">&nbsp;</a>
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="hidden w-6 h-6 text-indigo-500 absolute right-0 cursor-pointer"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6v12m6-6H6"
/>
</svg>
</div>
<a class="mt-2 block">
<p
class="mt-3 bg-slate-100 animate-pulse rounded-lg cursor-pointer w-3/4"
>
&nbsp;
</p>
<p
class="mt-3 bg-slate-100 animate-pulse rounded-lg cursor-pointer w-full"
>
&nbsp;
</p>
<p
class="mt-3 bg-slate-100 animate-pulse rounded-lg cursor-pointer w-2/3"
>
&nbsp;
</p>
<p
class="mt-3 bg-slate-100 animate-pulse rounded-lg cursor-pointer w-3/4"
>
&nbsp;
</p>
<p
class="mt-3 bg-slate-100 animate-pulse rounded-lg cursor-pointer w-3/4"
>
&nbsp;
</p>
</a>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
dataList: { type: Object, required: true },
pageTitle: { type: Object, required: true },
isLoading: { type: Boolean, default: false },
});
const bordPage = ['preach', 'programme', 'youtube'];
function pageMove(pageName, detail) {
if (pageName === 'notice') {
navigateTo('/support/' + pageName);
} else if (bordPage.includes(pageName) == true) {
if (detail == '') {
navigateTo('/board/' + pageName + '/list');
} else {
navigateTo('/board/' + pageName + '/view/' + detail.cid);
}
}
}
</script>

View File

@@ -0,0 +1,131 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<TransitionRoot as="template" :show="open">
<Dialog as="div" class="relative z-10" @close="open = false">
<div
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
/>
<div class="fixed z-10 inset-0 overflow-y-auto">
<div
class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0"
>
<DialogPanel
class="relative bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div
:class="
currentModalInfo['type'] == 'ok'
? 'mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10'
: currentModalInfo['type'] ==
'error'
? 'mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10'
: 'mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-gray-100 sm:mx-0 sm:h-10 sm:w-10'
"
>
<CheckIcon
v-if="currentModalInfo['type'] == 'ok'"
class="h-6 w-6 text-green-600"
aria-hidden="true"
/>
<ExclamationTriangleIcon
v-else-if="
currentModalInfo['type'] == 'error'
"
class="h-6 w-6 text-red-600"
aria-hidden="true"
/>
<ExclamationCircleIcon
v-else
class="h-6 w-6 text-gray-600"
aria-hidden="true"
/>
</div>
<div
class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"
>
<DialogTitle
as="h3"
class="text-lg leading-6 font-medium text-gray-900"
>
{{ currentModalInfo['title'] }}
</DialogTitle>
<div class="mt-2">
<p class="text-sm text-gray-500">
{{ currentModalInfo['message'] }}
</p>
</div>
</div>
</div>
</div>
<div
class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"
>
<button
type="button"
:class="
currentModalInfo['type'] == 'ok'
? 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm'
: currentModalInfo['type'] == 'error'
? 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm'
: 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-600 text-base font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:ml-3 sm:w-auto sm:text-sm'
"
@click="
_crossCtl.onModalClosed(
currentModalInfo.serial,
0
)
"
>
{{ currentModalInfo['btnTexts'][0] }}
</button>
<button
v-if="currentModalInfo['btnCount'] > 1"
ref="cancelButtonRef"
type="button"
:class="
currentModalInfo['type'] == 'ok'
? 'mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm'
: currentModalInfo['type'] == 'error'
? 'mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm'
: 'mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm'
"
@click="
_crossCtl.onModalClosed(
currentModalInfo.serial,
1
)
"
>
{{ currentModalInfo['btnTexts'][1] }}
</button>
</div>
</DialogPanel>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import {
Dialog,
DialogPanel,
DialogTitle,
TransitionRoot,
} from '@headlessui/vue';
import {
CheckIcon,
ExclamationTriangleIcon,
ExclamationCircleIcon,
} from '@heroicons/vue/24/outline';
const currentModalInfo = _crossCtl.currentModalInfo;
const open = computed(() => {
return currentModalInfo.value['serial'] !== -1;
});
</script>

View File

@@ -0,0 +1,222 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="flex-1 h-0 pt-5 pb-4 overflow-y-auto bg-white">
<div class="flex-shrink-0 flex items-center px-4">
<a href="javascript:void(0)" @click="router.push('/')">
<!--{{ siteName }}-->
<img
src="/kiso_ci_1.png"
alt="KISO CI"
width="500"
height="600"
/>
</a>
</div>
<nav class="flex-1 mt-5 px-2 space-y-1 bg-white" aria-label="Sidebar">
<template v-for="item in currentMenu['main']" :key="item.idx">
<div v-if="!item.subs">
<a
href="javascript:void(0)"
:class="[
item['current']
? 'bg-gray-100 text-gray-900'
: 'bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900',
'group w-full flex items-center pl-2 py-2 text-sm font-medium rounded-md',
]"
@click="handleItemClick(item)"
>
<component
:is="item['icon']"
:class="[
item['current']
? 'text-gray-500'
: 'text-gray-400 group-hover:text-gray-500',
'mr-3 flex-shrink-0 h-6 w-6',
]"
aria-hidden="true"
/>
{{ item['title'] }}
</a>
</div>
<Disclosure
v-else
v-slot="{ open, close }"
as="div"
class="space-y-1"
>
<DisclosureButton
:class="[
item['current']
? 'bg-gray-100 text-gray-900'
: 'bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900',
'group w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md',
]"
>
<component
:is="item['icon']"
class="mr-3 flex-shrink-0 h-6 w-6 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
<span class="flex-1">
{{ item.title }}
</span>
<svg
:class="[
open
? 'text-gray-400 rotate-90'
: 'text-gray-300',
'ml-3 flex-shrink-0 h-5 w-5 transform group-hover:text-gray-400 transition-colors ease-in-out duration-150',
]"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path d="M6 6L14 10L6 14V6Z" fill="currentColor" />
</svg>
</DisclosureButton>
<DisclosurePanel class="space-y-1">
<a
v-for="subItem in item.subs"
:key="subItem['idx']"
href="javascript:void(0)"
:class="[
subItem['current']
? 'bg-gray-100 text-gray-900'
: 'bg-white text-gray-600 hover:text-gray-900 hover:bg-gray-50',
'group w-full flex items-center pl-10 pr-2 py-2 text-sm font-medium rounded-md ',
]"
@click="handleSubItemClick(subItem)"
>
{{ subItem['title'] }}
</a>
</DisclosurePanel>
<button
v-show="false"
:ref="(el) => (elements[item.idx] = el)"
:data-id="item.idx"
@click="doClose(close)"
></button>
<DisclosureStateEmitter
:show="open"
@show="hideOther(item.idx)"
/>
</Disclosure>
</template>
</nav>
</div>
<div class="flex-shrink-0 flex bg-white p-4">
<a href="#" class="flex-shrink-0 group block">
<div class="flex items-center">
<div class="space-y-1">
<h3
id="projects-headline"
class="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider"
></h3>
<div
class="space-y-1"
role="group"
aria-labelledby="projects-headline"
>
<a
href="javascript:void(0)"
class="group flex items-center px-3 py-2 text-sm font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50"
@click="doSignInAndOut()"
>
<span class="truncate">
{{ isAuthenticated ? '로그아웃' : '로그인' }}
</span>
</a>
<a
v-for="item in currentMenu['sub']"
:key="item.idx"
href="javascript:void(0)"
class="group flex items-center px-3 py-2 text-sm font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50"
@click="handleItemClick(item)"
>
<span class="truncate">
{{ item.title }}
</span>
</a>
</div>
</div>
</div>
</a>
</div>
</template>
<script setup lang="ts">
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
const props = defineProps({
onMove: {
type: Function,
required: true,
},
});
const isAuthenticated = _crossCtl.isAuthenticated;
const siteName = ref(_siteConfig.siteName);
const router = useRouter();
const currentMenu = _crossCtl.menu;
const elements = ref([]);
async function doSignInAndOut() {
if (_crossCtl.isAuthenticated.value) {
const response = await _crossCtl.doComm('signout', '', {});
console.log('response=', response);
if (response['responseCode'] == 200) {
_crossCtl.setUserInfo({
isAdmin: false,
isApproved: false,
isAuthenticated: false,
isHighLeveled: false,
isOp: false,
isSuperOp: false,
});
_crossCtl.setUserProfile({});
isAuthenticated.value = false;
window.location.href = '/';
} else {
alert(response['responseMessage']);
}
} else {
console.log("go user login");
navigateTo({
path: '/user/signin',
});
}
}
function handleItemClick(item) {
hideOther(Number.MAX_SAFE_INTEGER);
_crossCtl.moveToMenuItem(item);
props.onMove();
}
function handleSubItemClick(item) {
_crossCtl.moveToMenuItem(item);
props.onMove();
}
function hideOther(id) {
let targetEl = null;
const items = elements.value.filter((elm) => {
if (elm.getAttribute('data-id') == id.toString()) {
targetEl = elm;
}
return elm.getAttribute('data-id') !== id.toString();
// return true;
});
items.forEach((elm) => elm.click());
// targetEl.click();
}
function doClose(close) {
close();
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<div
class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800 border-2 m-5"
>
<div class="ml-4">
<dt class="mt-3 text-lg leading-6 font-medium text-gray-900">
{{ item.title }}
</dt>
<dd
class="mt-9 text-base text-gray-500"
v-html="
_utils
.escapeHtml(item.detail)
.replace(/(?:\r\n|\r|\n)/g, '<br />')
"
></dd>
</div>
<p class="text-right text-xs">
{{ $customFormat(item.created) }}
</p>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
item: { type: Object, required: true },
});
</script>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,112 @@
<template>
<footer
class="footer px-10 py-4 bg-base-200 text-base-content border-base-300"
>
<div class="items-center grid-flow-col">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
class="fill-current"
>
<path
d="M22.672 15.226l-2.432.811.841 2.515c.33 1.019-.209 2.127-1.23 2.456-1.15.325-2.148-.321-2.463-1.226l-.84-2.518-5.013 1.677.84 2.517c.391 1.203-.434 2.542-1.831 2.542-.88 0-1.601-.564-1.86-1.314l-.842-2.516-2.431.809c-1.135.328-2.145-.317-2.463-1.229-.329-1.018.211-2.127 1.231-2.456l2.432-.809-1.621-4.823-2.432.808c-1.355.384-2.558-.59-2.558-1.839 0-.817.509-1.582 1.327-1.846l2.433-.809-.842-2.515c-.33-1.02.211-2.129 1.232-2.458 1.02-.329 2.13.209 2.461 1.229l.842 2.515 5.011-1.677-.839-2.517c-.403-1.238.484-2.553 1.843-2.553.819 0 1.585.509 1.85 1.326l.841 2.517 2.431-.81c1.02-.33 2.131.211 2.461 1.229.332 1.018-.21 2.126-1.23 2.456l-2.433.809 1.622 4.823 2.433-.809c1.242-.401 2.557.484 2.557 1.838 0 .819-.51 1.583-1.328 1.847m-8.992-6.428l-5.01 1.675 1.619 4.828 5.011-1.674-1.62-4.829z"
></path>
</svg>
<p>{{ copyrightName }} <br />{{ siteSlogan }}</p>
</div>
<div class="md:place-self-center md:justify-self-end">
<div class="grid grid-flow-col gap-4">
<a v-for="item in snsLinks" :key="item.tag" :href="item.url">
<component
:is="socialLogs[item.tag].icon"
v-if="socialLogs[item.tag] != undefined"
class="h-6 w-6"
aria-hidden="true"
/>
<span v-else class="">{{ item.tag }}</span>
</a>
</div>
</div>
</footer>
</template>
<script setup lang="ts">
const copyrightName = ref(_siteConfig.copyrightName);
const siteSlogan = ref(_siteConfig.siteSlogan);
const snsLinks = ref(_siteConfig.snsLinks);
const socialLogs = {
facebook: {
name: 'Facebook',
href: '#',
icon: defineComponent({
render: () =>
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
h('path', {
'fill-rule': 'evenodd',
d: 'M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z',
'clip-rule': 'evenodd',
}),
]),
}),
},
instagram: {
name: 'Instagram',
href: '#',
icon: defineComponent({
render: () =>
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
h('path', {
'fill-rule': 'evenodd',
d: 'M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z',
'clip-rule': 'evenodd',
}),
]),
}),
},
twitter: {
name: 'Twitter',
href: '#',
icon: defineComponent({
render: () =>
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
h('path', {
d: 'M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84',
}),
]),
}),
},
gitHub: {
name: 'GitHub',
href: '#',
icon: defineComponent({
render: () =>
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
h('path', {
'fill-rule': 'evenodd',
d: 'M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z',
'clip-rule': 'evenodd',
}),
]),
}),
},
dribbble: {
name: 'Dribbble',
href: '#',
icon: defineComponent({
render: () =>
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
h('path', {
'fill-rule': 'evenodd',
d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2zm6.605 4.61a8.502 8.502 0 011.93 5.314c-.281-.054-3.101-.629-5.943-.271-.065-.141-.12-.293-.184-.445a25.416 25.416 0 00-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362zM12 3.475c2.17 0 4.154.813 5.662 2.148-.152.216-1.443 1.941-4.48 3.08-1.399-2.57-2.95-4.675-3.189-5A8.687 8.687 0 0112 3.475zm-3.633.803a53.896 53.896 0 013.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.581 8.581 0 014.729-5.975zM3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215.25.477.477.965.694 1.453-.109.033-.228.065-.336.098-4.404 1.42-6.747 5.303-6.942 5.629a8.522 8.522 0 01-2.19-5.705zM12 20.547a8.482 8.482 0 01-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337.022-.01.033-.01.054-.022a35.318 35.318 0 011.823 6.475 8.4 8.4 0 01-3.341.684zm4.761-1.465c-.086-.52-.542-3.015-1.659-6.084 2.679-.423 5.022.271 5.314.369a8.468 8.468 0 01-3.655 5.715z',
'clip-rule': 'evenodd',
}),
]),
}),
},
};
</script>

View File

@@ -0,0 +1,289 @@
<template>
<div
class="pt-5 px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6"
>
<div class="flex-1 flex justify-between sm:hidden">
<a
href="#"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:text-gray-500"
>
Previous
</a>
<a
href="#"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:text-gray-500"
>
Next
</a>
</div>
<div
class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"
>
<div>
<p class="text-sm text-gray-700">
Showing
<span class="font-medium">{{ showingFrom }}</span>
to
<span class="font-medium">{{ showingTo }}</span>
of
<span class="font-medium">{{ recordsTotal }}</span>
results
</p>
</div>
<div>
<nav
class="relative z-0 inline-flex shadow-sm -space-x-px"
aria-label="Pagination"
>
<a
href="javascript:void(0)"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
@click="gotoPage('first', 0)"
>
<span class="sr-only">First</span>
<!-- Heroicon name: chevron-double-left -->
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
></path>
</svg>
</a>
<a
href="javascript:void(0)"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
@click="gotoPage('prev', 0)"
>
<span class="sr-only">Previous</span>
<!-- Heroicon name: chevron-left -->
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
</a>
<a
v-for="(item, index) in currentSlots"
:key="index"
href="javascript:void(0)"
:class="
currentSlots[index] == currentPageNumber
? 'bg-gray-200'
: 'bg-white'
"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
@click="gotoPage(currentSlots[index], index)"
>
{{ currentSlots[index] }}
</a>
<a
href="javascript:void(0)"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
@click="gotoPage('next', 0)"
>
<span class="sr-only">Next</span>
<!-- Heroicon name: chevron-right -->
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"
/>
</svg>
</a>
<a
href="javascript:void(0)"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
@click="gotoPage('last', 0)"
>
<span class="sr-only">Last</span>
<!-- Heroicon name: chevron-double-right -->
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 5l7 7-7 7M5 5l7 7-7 7"
></path>
</svg>
</a>
</nav>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ChevronLeftIcon,
ChevronRightIcon,
} from '@heroicons/vue/solid/index.js';
import { computed } from 'vue';
const props = defineProps({
totalPageCount: { type: Number, default: 1 },
currentPageNumber: { type: Number, default: 1 },
pageSize: { type: Number, default: 10 },
recordsTotal: { type: Number, default: 0 },
pageMove: { type: Function, required: true },
});
const pagenationSize = 5;
//console.log(props);
const showingFrom = computed(() => {
const result = 1 + (props.currentPageNumber - 1) * props.pageSize;
return result;
});
//console.log(showingFrom.value);
const showingTo = computed(() => {
let result = props.currentPageNumber * props.pageSize;
if (result > props.recordsTotal) {
result = props.recordsTotal;
}
return result;
});
const currentSlots = computed(() => {
const result: string[] = [];
if (props.totalPageCount > pagenationSize) {
if (props.currentPageNumber < Math.ceil(pagenationSize / 2)) {
for (let i = 1; i < pagenationSize; i++) {
// good for 3, 4, ....
result.push(i.toString());
}
result.push('...');
} else if (
props.currentPageNumber >
props.totalPageCount - pagenationSize / 2
) {
result.push('...');
for (
let i = props.totalPageCount - pagenationSize + 1;
i <= props.totalPageCount;
i++
) {
// good for 3, 4, ....
result.push(i.toString());
}
} else {
result.push('...');
for (
let i =
props.currentPageNumber - Math.floor(pagenationSize / 2);
i < props.currentPageNumber + Math.ceil(pagenationSize / 2);
i++
) {
// good for 3, 4, ....
result.push(i.toString());
}
result.push('...');
}
} else {
for (let i = 1; i <= props.totalPageCount; i++) {
result.push(i.toString());
}
}
//console.log('result = ', result);
return result;
});
function gotoPage(target, opt) {
//console.log('gotoPage, target=', target);
let targetPageIndex = props.currentPageNumber;
switch (target) {
case 'first':
targetPageIndex = 1;
break;
case 'prev':
if (props.currentPageNumber > 1) {
targetPageIndex = props.currentPageNumber - 1;
} else {
targetPageIndex = props.currentPageNumber;
}
break;
case 'next':
if (props.totalPageCount > props.currentPageNumber) {
targetPageIndex = props.currentPageNumber + 1;
} else {
targetPageIndex = props.currentPageNumber;
}
break;
case 'last':
targetPageIndex = props.totalPageCount;
break;
case '...':
if (opt == 0) {
if (props.currentPageNumber - pagenationSize > 0) {
targetPageIndex = props.currentPageNumber - pagenationSize;
} else {
targetPageIndex = 1;
}
} else {
if (
props.currentPageNumber + pagenationSize <
props.totalPageCount
) {
targetPageIndex = props.currentPageNumber + pagenationSize;
} else {
targetPageIndex = props.totalPageCount;
}
}
break;
default:
const tmpPageIdx = parseInt(target);
if (tmpPageIdx >= 1 && tmpPageIdx <= props.totalPageCount) {
targetPageIndex = tmpPageIdx;
} else {
targetPageIndex = props.currentPageNumber;
}
}
//console.log('final targetPageIdex = ', targetPageIndex);
// console.log('huk', this);
// this.$parent.pageMove(targetPageIndex);
// $emit('pageMove', targetPageIndex);
props.pageMove(targetPageIndex);
}
</script>

View File

@@ -0,0 +1,85 @@
<template>
<div class="">
<div class="card border overflow-x-auto">
<table class="table w-full">
<!-- head -->
<thead>
<tr>
<th
v-for="(heading, index) in headings"
:key="index"
:width="
heading['widthRatio'] != ''
? heading['widthRatio'] + '%'
: '100%'
"
scope="col"
>
{{ heading['title'] }}
</th>
<th
v-for="(action, index) in actions"
:key="index + headings.length"
:width="actions.length * 1"
scope="col"
>
<span class="sr-only">{{ action }}</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, itemIndex) in data" :key="itemIndex">
<td v-for="(heading, index) in headings" :key="index">
{{
columnFilter
? columnFilter(
heading['key'],
item[heading['key']]
)
: item[heading['key']]
}}
</td>
<td>
<button
v-for="(action, index) in actions"
:key="index"
class="btn btn-sm btn-primary"
:class="index != 0 ? 'ml-1' : ''"
@click="doAction(action, item[actionKey])"
>
{{ action
}}<span class="sr-only"
>, {{ item['serial'] }}</span
>
</button>
</td>
</tr>
<tr v-if="data.length == 0">
<td>{{ 'No Data to display...' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
headings: { type: Array<object>, required: true },
actions: { type: Array<string>, default: [] },
data: { type: Array<object>, required: true },
actionKey: { type: String, default: 'serial' },
columnFilter: {
type: Function,
default: (key: string, val: string) => val,
},
doAction: {
type: Function,
default: (key: string) => {
return key;
},
},
});
</script>

View File

@@ -0,0 +1,110 @@
<template>
<div class="flex flex-col">
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div
class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8"
>
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th
v-for="(heading, index) in headings"
:key="index"
:width="
heading['widthRatio'] != ''
? heading['widthRatio'] + '%'
: '100%'
"
:class="
index == 0
? 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 md:pl-0'
: 'py-3.5 px-3 text-left text-sm font-semibold text-gray-900'
"
scope="col"
>
{{ heading['title'] }}
</th>
<th
v-for="(action, index) in actions"
:key="index + headings.length"
:width="actions.length * 1"
class="relative py-3.5 pl-3 pr-4 sm:pr-6 md:pr-0"
scope="col"
>
<span class="sr-only">{{ action }}</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="(item, itemIndex) in data" :key="itemIndex">
<td
v-for="(heading, index) in headings"
:key="index"
:class="
index == 0
? 'whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 md:pl-0'
: 'whitespace-nowrap py-4 px-3 text-sm text-gray-500'
"
>
{{
columnFilter
? columnFilter(
heading['key'],
item[heading['key']]
)
: item[heading['key']]
}}
</td>
<td
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 md:pr-0"
>
<a
v-for="(action, index) in actions"
:key="index"
href="javascript:void(0)"
:class="index != 0 ? 'ml-3' : ''"
class="text-indigo-600 hover:text-indigo-900"
@click="doAction(action, item[actionKey])"
>{{ action
}}<span class="sr-only"
>, {{ item['serial'] }}</span
></a
>
</td>
</tr>
<tr v-if="data.length == 0">
<td
:colspan="headings.length"
class="whitespace-nowrap py-4 px-3 text-sm text-gray-500"
>
{{ noDataMessage }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
headings: { type: Array<object>, required: true },
actions: { type: Array<string>, default: [] },
data: { type: Array<object>, required: true },
actionKey: { type: String, default: 'serial' },
columnFilter: {
type: Function,
default: (key: string, val: string) => val,
},
doAction: {
type: Function,
default: (key: string) => {
return key;
},
},
noDataMessage: { type: String, default: '조회된 데이터가 없습니다.' },
});
</script>

View File

@@ -0,0 +1,25 @@
<template>
<div>
<BaseAvater1
:image-size="photoSize"
:image-url="photoUrl"
:image-alt="photoAlt"
/>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
imageSize: { type: Number, default: 12 },
});
const photoSize = ref(props.imageSize);
const photoUrl = _crossCtl.profileUrlRef;
const photoAlt = ref(
_crossCtl.isAuthenticated
? _crossCtl.userInfo['userInfo']['userName'] + '의 프로필 사진'
: ''
);
</script>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,22 @@
<template>
<div v-show="false"></div>
</template>
<script>
export default {
props: {
show: {
type: Boolean,
},
},
emits: ['show', 'hide'],
watch: {
show(show) {
if (show) {
this.$emit('show');
} else {
this.$emit('hide');
}
},
},
};
</script>

View File

@@ -0,0 +1,106 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<footer class="bg-white">
<div
class="mx-auto max-w-7xl py-12 px-4 sm:px-6 md:flex md:items-center md:justify-between lg:px-8"
>
<div class="flex justify-center space-x-6 md:order-2">
<a
v-for="item in snsLinks"
:key="item.tag"
:href="item.url"
class="text-gray-400 hover:text-gray-500"
>
<span class="sr-only">{{ item.tag }}</span>
<component
:is="socialLogos[item.tag].icon"
v-if="socialLogos[item.tag] != undefined"
class="h-6 w-6"
aria-hidden="true"
/>
</a>
</div>
<div class="mt-8 md:order-1 md:mt-0">
<p class="text-center text-base text-gray-400">
{{ copyrightName }}
</p>
</div>
</div>
</footer>
</template>
<script setup lang="ts">
const copyrightName = ref(_siteConfig.copyrightName);
const snsLinks = ref(_siteConfig.snsLinks);
const socialLogos = {
facebook: {
name: 'Facebook',
href: '#',
icon: defineComponent({
render: () =>
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
h('path', {
'fill-rule': 'evenodd',
d: 'M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z',
'clip-rule': 'evenodd',
}),
]),
}),
},
instagram: {
name: 'Instagram',
href: '#',
icon: defineComponent({
render: () =>
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
h('path', {
'fill-rule': 'evenodd',
d: 'M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z',
'clip-rule': 'evenodd',
}),
]),
}),
},
twitter: {
name: 'Twitter',
href: '#',
icon: defineComponent({
render: () =>
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
h('path', {
d: 'M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84',
}),
]),
}),
},
github: {
name: 'GitHub',
href: '#',
icon: defineComponent({
render: () =>
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
h('path', {
'fill-rule': 'evenodd',
d: 'M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z',
'clip-rule': 'evenodd',
}),
]),
}),
},
dribbble: {
name: 'Dribbble',
href: '#',
icon: defineComponent({
render: () =>
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
h('path', {
'fill-rule': 'evenodd',
d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2zm6.605 4.61a8.502 8.502 0 011.93 5.314c-.281-.054-3.101-.629-5.943-.271-.065-.141-.12-.293-.184-.445a25.416 25.416 0 00-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362zM12 3.475c2.17 0 4.154.813 5.662 2.148-.152.216-1.443 1.941-4.48 3.08-1.399-2.57-2.95-4.675-3.189-5A8.687 8.687 0 0112 3.475zm-3.633.803a53.896 53.896 0 013.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.581 8.581 0 014.729-5.975zM3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215.25.477.477.965.694 1.453-.109.033-.228.065-.336.098-4.404 1.42-6.747 5.303-6.942 5.629a8.522 8.522 0 01-2.19-5.705zM12 20.547a8.482 8.482 0 01-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337.022-.01.033-.01.054-.022a35.318 35.318 0 011.823 6.475 8.4 8.4 0 01-3.341.684zm4.761-1.465c-.086-.52-.542-3.015-1.659-6.084 2.679-.423 5.022.271 5.314.369a8.468 8.468 0 01-3.655 5.715z',
'clip-rule': 'evenodd',
}),
]),
}),
},
};
</script>

View File

@@ -0,0 +1,476 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<Disclosure v-slot="{ open }" as="nav" class="bg-white shadow">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between">
<div class="flex">
<div class="flex flex-shrink-0 items-center">
<a
href="javascript:void(0)"
@click="handleItemClick($event, null)"
>
<img
class="block h-8 w-auto lg:hidden"
:src="siteLogoUrl"
alt="site logo image"
/>
<img
class="hidden h-8 w-auto lg:block"
:src="siteLogoUrl"
alt="site logo image"
/>
</a>
</div>
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
<!-- Current: "border-indigo-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" -->
<template
v-for="item in currentMenu['main']"
:key="item.idx"
>
<a
v-if="item.subs == undefined"
href="javascript:void(0)"
:class="
isCurrentMenu(item)
? 'inline-flex items-center border-b-2 border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900'
: 'inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700'
"
@click="handleItemClick($event, item)"
>{{ item['title'] }}</a
>
<div
v-else
:class="
isCurrentMenu(item)
? 'inline-flex items-center border-b-2 border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900'
: 'inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700'
"
>
<!-- dropdown -->
<Menu as="div" class="relative ml-3">
<div>
<MenuButton class="flex">
{{ item['title'] }}
<svg
x-state:on="Item active"
x-state:off="Item inactive"
class="h-5 w-5 group-hover:text-gray-500 text-gray-400"
x-bind:class="{ 'text-gray-600': flyoutMenuOpen, 'text-gray-400': !flyoutMenuOpen }"
x-description="Heroicon name: chevron-down"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</MenuButton>
</div>
<transition
enter-active-class="transition ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<MenuItem
v-for="subItem in item['subs']"
v-slot="{ active }"
:key="subItem.idx"
>
<a
href="javascript:void(0)"
:class="[
active
? 'bg-gray-100'
: '',
'block px-4 py-2 text-sm text-gray-700',
]"
@click="
handleItemClick(
$event,
subItem
)
"
>{{ subItem.title }}</a
>
</MenuItem>
</MenuItems>
</transition>
</Menu>
</div>
</template>
<template
v-for="item in currentMenu['sub']"
:key="item.idx"
>
<a
v-if="item.subs == undefined"
href="javascript:void(0)"
:class="
isCurrentMenu(item)
? 'inline-flex items-center border-b-2 border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900'
: 'inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700'
"
@click="handleItemClick($event, item)"
>{{ item['title'] }}</a
>
<div
v-else
:class="
isCurrentMenu(item)
? 'inline-flex items-center border-b-2 border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900'
: 'inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700'
"
>
<!-- dropdown -->
<Menu as="div" class="relative ml-3">
<div>
<MenuButton class="flex">
{{ item['title'] }}
<svg
x-state:on="Item active"
x-state:off="Item inactive"
class="h-5 w-5 group-hover:text-gray-500 text-gray-400"
x-bind:class="{ 'text-gray-600': flyoutMenuOpen, 'text-gray-400': !flyoutMenuOpen }"
x-description="Heroicon name: chevron-down"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</MenuButton>
</div>
<transition
enter-active-class="transition ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<MenuItem
v-for="subItem in item['subs']"
v-slot="{ active }"
:key="subItem.idx"
>
<a
href="javascript:void(0)"
:class="[
active
? 'bg-gray-100'
: '',
'block px-4 py-2 text-sm text-gray-700',
]"
@click="
handleItemClick(
$event,
subItem
)
"
>{{ subItem.title }}</a
>
</MenuItem>
</MenuItems>
</transition>
</Menu>
</div>
</template>
</div>
</div>
<div class="hidden sm:ml-6 sm:flex sm:items-center">
<button
v-if="false"
type="button"
class="rounded-full bg-white p-1 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
<span class="sr-only">View notifications</span>
<BellIcon class="h-6 w-6" aria-hidden="true" />
</button>
<!-- Profile dropdown -->
<Menu as="div" class="relative ml-3">
<div>
<MenuButton
class="flex rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
<span class="sr-only">Open user menu</span>
<BaseUserProfileImage :image-size="2" />
</MenuButton>
</div>
<transition
enter-active-class="transition ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<MenuItem
v-if="isAuthenticated"
v-slot="{ active }"
>
<a
href="javascript:void(0)"
:class="[
active ? 'bg-gray-100' : '',
'block px-4 py-2 text-sm text-gray-700',
]"
@click="moveToPath('/user/info')"
>설정</a
>
</MenuItem>
<MenuItem v-slot="{ active }">
<a
href="javascript:void(0)"
:class="[
active ? 'bg-gray-100' : '',
'block px-4 py-2 text-sm text-gray-700',
]"
@click="doSignInAndOut($event)"
>{{
isAuthenticated == true
? '로그아웃'
: '로그인'
}}</a
>
</MenuItem>
</MenuItems>
</transition>
</Menu>
</div>
<div class="-mr-2 flex items-center sm:hidden">
<!-- Mobile menu button -->
<DisclosureButton
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
>
<span class="sr-only">Open main menu</span>
<Bars3Icon
v-if="!open"
class="block h-6 w-6"
aria-hidden="true"
/>
<XMarkIcon
v-else
class="block h-6 w-6"
aria-hidden="true"
/>
</DisclosureButton>
</div>
</div>
</div>
<DisclosurePanel class="sm:hidden">
<div class="space-y-1 pt-2 pb-3">
<!-- Current: "bg-indigo-50 border-indigo-500 text-indigo-700", Default: "border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700" -->
<template v-for="item in currentMenu['main']" :key="item.idx">
<DisclosureButton
v-if="item.path != undefined"
as="a"
href="javascript:void(0)"
:class="
currentRoutePath == item.path
? 'block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700'
: 'block border-l-4 border-transparent py-2 pl-3 pr-4 text-base font-medium text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
"
@click="handleItemClick($event, item)"
>{{ item['title'] }}</DisclosureButton
>
<a
v-else
href="javascript:void(0)"
:class="
currentRoutePath == item.path
? 'block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700'
: 'block border-l-4 border-transparent py-2 pl-3 pr-4 text-base font-medium text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
"
>{{ item['title'] }}</a
>
<template v-for="subItem in item.subs" :key="subItem.idx">
<DisclosureButton
v-if="subItem.path != undefined"
as="a"
href="javascript:void(0)"
:class="
currentRoutePath == subItem.path
? 'block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700'
: 'block border-l-4 border-transparent py-2 pl-3 pr-4 text-base font-medium text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
"
class="ml-4"
@click="handleItemClick($event, subItem)"
>{{ subItem['title'] }}</DisclosureButton
>
<a
v-else
href="javascript:void(0)"
:class="
currentRoutePath == subItem.path
? 'block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700'
: 'block border-l-4 border-transparent py-2 pl-3 pr-4 text-base font-medium text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
"
>{{ subItem['title'] }}</a
>
</template>
</template>
<template v-for="item in currentMenu['sub']" :key="item.idx">
<DisclosureButton
as="a"
href="javascript:void(0)"
:class="
currentRoutePath == item.path
? 'block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700'
: 'block border-l-4 border-transparent py-2 pl-3 pr-4 text-base font-medium text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
"
@click="handleItemClick($event, item)"
>{{ item['title'] }}</DisclosureButton
>
</template>
</div>
<div class="border-t border-gray-200 pt-4 pb-3">
<div class="flex items-center px-4">
<div class="flex-shrink-0">
<BaseUserProfileImage :image-size="3" />
</div>
<div v-if="false" class="ml-3">
<div class="text-base font-medium text-gray-800">
Tom Cook
</div>
<div class="text-sm font-medium text-gray-500">
tom@example.com
</div>
</div>
<button
v-if="false"
type="button"
class="ml-auto flex-shrink-0 rounded-full bg-white p-1 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
<span class="sr-only">View notifications</span>
<BellIcon class="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div class="mt-3 space-y-1">
<DisclosureButton
v-if="isAuthenticated"
as="a"
href="javascript:void(0)"
class="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800"
@click="moveToPath('/user/info')"
>설정</DisclosureButton
>
<DisclosureButton
as="a"
href="javascript:void(0)"
class="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800"
@click="doSignInAndOut($event)"
>{{
isAuthenticated == true ? '로그아웃' : '로그인'
}}</DisclosureButton
>
</div>
</div>
</DisclosurePanel>
</Disclosure>
</template>
<script setup lang="ts">
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
Menu,
MenuButton,
MenuItem,
MenuItems,
} from '@headlessui/vue';
import { Bars3Icon, BellIcon, XMarkIcon } from '@heroicons/vue/24/outline';
const route = useRoute();
const currentRoutePath = ref(route.path);
const siteLogoUrl = ref(_siteConfig.siteLogoUrl);
const currentMenu = ref(_crossCtl.menu);
const isAuthenticated = ref(_crossCtl.isAuthenticated);
function isCurrentMenu(menuItem) {
let finalResult = false;
if (menuItem.path == currentRoutePath.value) {
finalResult = true;
} else {
if (menuItem['subs'] != undefined) {
for (let i = 0; i < menuItem['subs'].length; i++) {
if (menuItem['subs'][i].path == currentRoutePath.value) {
finalResult = true;
break;
}
}
}
}
return finalResult;
}
function handleItemClick(e, item) {
if (item == null) {
navigateTo('/');
currentRoutePath.value = '/';
} else {
_crossCtl.moveToMenuItem(item);
currentRoutePath.value = item.path;
}
}
function moveToPath(path) {
navigateTo(path);
currentRoutePath.value = path;
}
async function doSignInAndOut(e) {
e.target.blur();
if (_crossCtl.isAuthenticated.value) {
const response = await _crossCtl.doComm('signout', '', {});
console.log('response=', response);
if (response['responseCode'] == 200) {
_crossCtl.setUserInfo({
isAdmin: false,
isApproved: false,
isAuthenticated: false,
isHighLeveled: false,
isOp: false,
isSuperOp: false,
});
_crossCtl.setUserProfile({});
isAuthenticated.value = false;
window.location.href = '/';
} else {
alert(response['responseMessage']);
}
} else {
navigateTo({
path: '/user/signin',
});
}
}
</script>

View File

@@ -0,0 +1,27 @@
import { defineComponent, h, PropType } from 'vue';
import VueJsonPretty from 'vue-json-pretty';
export default defineComponent({
name: 'VueJsonPretty',
components: {
VueJsonPretty,
},
props: {
path: {
type: String,
default: 'res',
},
data: {
type: Object,
default: () => {},
},
},
setup(props) {
return () =>
h(VueJsonPretty, {
path: props.path,
data: props.data,
});
},
});