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,103 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div>
<div class="bg-gray-50 pt-12 sm:pt-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-4xl mx-auto text-center">
<h2
class="text-3xl font-extrabold text-gray-900 sm:text-4xl"
>
어드민 대시보드
</h2>
<p class="mt-3 text-xl text-gray-500 sm:mt-4">
서비스 현황 개괄이 표시될 페이지 입니다.
</p>
</div>
</div>
<div class="mt-10 pb-12 bg-white sm:pb-16">
<div class="relative">
<div class="absolute inset-0 h-1/2 bg-gray-50" />
<div
class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
>
<div class="max-w-4xl mx-auto">
<dl
class="rounded-lg bg-white shadow-lg sm:grid sm:grid-cols-3"
>
<div
class="flex flex-col border-b border-gray-100 p-6 text-center sm:border-0 sm:border-r"
>
<dt
class="order-2 mt-2 text-lg leading-6 font-medium text-gray-500"
>
word watched
</dt>
<dd
class="order-1 text-5xl font-extrabold text-indigo-600"
>
{{ formattedCount }}
</dd>
</div>
<div
class="flex flex-col border-t border-b border-gray-100 p-6 text-center sm:border-0 sm:border-l sm:border-r"
>
<dt
class="order-2 mt-2 text-lg leading-6 font-medium text-gray-500"
>
hit count
</dt>
<dd
class="order-1 text-5xl font-extrabold text-indigo-600"
>
{{ formattedHitCount }}
</dd>
</div>
<div
class="flex flex-col border-t border-gray-100 p-6 text-center sm:border-0 sm:border-l"
>
<dt
class="order-2 mt-2 text-lg leading-6 font-medium text-gray-500"
>
avg response time
</dt>
<dd
class="order-1 text-5xl font-extrabold text-indigo-600"
>
{{ elapsedAvg.toFixed(2) }}ms
</dd>
</div>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'check-auth-admin',
});
const wordCount = ref(0);
const hitCount = ref(0);
const elapsedAvg = ref(0);
const formattedCount = computed(() => {
return _utils.formatNumberWithComma(wordCount.value);
});
const formattedHitCount = computed(() => {
return _utils.formatNumberWithComma(hitCount.value);
});
const responseJson = await _crossCtl.doComm('local/select', 'dashboard', {});
console.log('responseJson=', responseJson);
if (responseJson['responseMessage'] == 'ok') {
wordCount.value = responseJson['data']['wordCount'];
hitCount.value = responseJson['data']['hitCount'];
elapsedAvg.value = responseJson['data']['elapsedAvg'] * 1000;
}
</script>

View File

@@ -0,0 +1,218 @@
<!--
This example requires Tailwind CSS v2.0+
This example requires some changes to your config:
```
// tailwind.config.js
module.exports = {
// ...
plugins: [
// ...
require('@tailwindcss/forms'),
],
}
```
-->
<template>
<div class="m-8">
<form
class="space-y-8 divide-y divide-gray-200"
@submit.prevent="doFilter"
>
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
<div>
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
필터 테스트
</h1>
<p class="mt-2 text-sm text-gray-700">
필터에 문장을 넣어 테스트 합니다.
</p>
</div>
<img
v-if="inPregressFlag"
width="32"
src="/loading-load-2.gif"
/>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button
type="button"
class="inline-flex items-center justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 sm:w-auto"
@click="doSyncFromFile"
>
DB 초기화
</button>
<button
type="button"
class="ml-3 inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"
@click="$router.push('/admin/filter/word/list')"
>
표현 관리
</button>
<button
type="submit"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
검사하기
</button>
</div>
</div>
<div class="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
<div>
<fieldset class="mt-4">
<legend class="sr-only">Filter Mode</legend>
<div class="space-y-4">
<div
v-for="notificationMethod in notificationMethods"
:key="notificationMethod.id"
class="flex items-center"
>
<input
:id="notificationMethod.id"
v-model="filterMode"
:value="notificationMethod.id"
name="notification-method"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
:for="notificationMethod.id"
class="ml-3 block text-sm font-medium text-gray-700"
>
{{ notificationMethod.title }}
</label>
</div>
</div>
</fieldset>
</div>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5"
>
<label
for="filterSource"
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
검사할 문장 입력
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<textarea
id="filterSource"
v-model="filterSource"
name="filterSource"
rows="3"
class="max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md"
/>
<p class="mt-2 text-sm text-gray-500">
필터 테스트를 위한 문장을 입력하고 아래
검사하기 버튼을 누르세요. <br />
(브라우저에서 측정한 실행시간 :
{{ elispe.toFixed(2) }} 밀리초. 네트워크
전송 지연 포함.)
</p>
<pre>{{ resultJson }}</pre>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</template>
<script setup lang="ts">
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
definePageMeta({
middleware: 'check-auth-admin',
});
const notificationMethods = [
{
id: 'quick',
title: 'quick. 첫번째 매칭이 발견되면 더 이상 검사하지 않고 바로 결과를 리턴',
},
{ id: 'normal', title: 'normal. 전체를 검사하여 모든 매칭을 리턴' },
{
id: 'filter',
title: 'filter. 전체를 검사하며 모든 매칭을 마스크로 치환한 결과까지 리턴',
},
];
const filterMode = ref('filter');
const filterSource = ref('테스트 할 문장을 이곳에 넣어 주세요.');
const resultJson = ref({});
const elispe = ref(0);
const inPregressFlag = ref(false);
async function doFilter() {
if (inPregressFlag.value == true) {
alert('이전 동작이 아직 진행중입니다.');
return;
}
inPregressFlag.value = true;
const startTime = performance.now();
const responseJson = await _crossCtl.doFilter('', {
text: filterSource.value,
mode: filterMode.value,
});
const endTime = performance.now();
inPregressFlag.value = false;
elispe.value = endTime - startTime;
// console.log('responseJson=', responseJson);
const result = responseJson;
resultJson.value = result;
if (responseJson['Status']['Code'] == 2000) {
// alert(result.length + ' match found');
} else {
alert(responseJson['Status']['Message']);
}
}
async function doSyncFromFile() {
if (inPregressFlag.value == true) {
alert('이전 동작이 아직 진행중입니다.');
return;
}
if (
window.confirm(
'이 기능은 DB에 저장된 모든 필터 단어를 삭제하고 원본 소스 파일의 내용으로 초기화 합니다. 실행하시겠습니까? (최소한 수십초가 소요되며 완료되면 ok라는 메세지가 출력됩니다.)'
)
) {
inPregressFlag.value = true;
const responseJson = await _crossCtl.doComm(
'local/update',
'filter',
{}
);
inPregressFlag.value = false;
// console.log('huk filter update result = ', responseJson);
alert(
responseJson['responseMessage'] +
' with ' +
_utils.formatNumberInBytesStyle(
parseInt(responseJson['wordCount']),
2
) +
' words'
);
}
}
</script>

View File

@@ -0,0 +1,342 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="divide-y divide-gray-200">
<form class="lg:col-span-9">
<!-- Profile section -->
<div class="py-6 px-4 sm:p-6 lg:pb-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
필터 단어 정보 확인, 변경
</h1>
<p class="mt-2 text-sm text-gray-700">
필터 단어의 세부 내용을 확인하고 수정합니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<img
v-if="inPregressFlag"
width="32"
src="/loading-load-2.gif"
/>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button
type="button"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="gotoAdminKeyLog()"
>
로그
</button>
</div>
</div>
<div class="mt-6 grid grid-cols-12 gap-6">
<div class="col-span-12 sm:col-span-6">
<label
for="by"
class="block text-sm font-medium text-gray-700"
>등록자</label
>
<input
id="by"
v-model="by"
disabled
type="text"
name="by"
autocomplete="by"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
/>
</div>
<div class="col-span-12 sm:col-span-6"></div>
</div>
<div class="mt-6 flex flex-col lg:flex-row">
<div class="flex-grow space-y-6">
<div>
<label
for="raw"
class="block text-sm font-medium text-gray-700"
>
모욕적인 표현
</label>
<div class="mt-1 rounded-md shadow-sm flex">
<input
id="raw"
v-model="raw"
disabled
type="text"
name="raw"
autocomplete="raw"
class="focus:ring-sky-500 focus:border-sky-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"
/>
</div>
</div>
<div
class="space-y-6 sm:space-y-5 divide-y divide-gray-200"
>
<div class="pt-6 sm:pt-5">
<div
role="group"
aria-labelledby="label-notifications"
>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline"
>
<div>
<div
id="label-notifications"
class="text-base font-medium text-gray-900 sm:text-sm sm:text-gray-700"
>
필터 단계
</div>
</div>
<div class="sm:col-span-2">
<div class="max-w-lg">
<p
class="text-sm text-gray-500"
>
필터 대상이 되는 단계를
선택합니다. (현재는 최대
필터만 동작)
</p>
<div class="mt-4 space-y-4">
<div
class="flex items-center"
>
<input
id="filter-high"
v-model="level"
value="10"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-high"
class="ml-3 block text-sm font-medium text-gray-700"
>
최대 필터
</label>
</div>
<div
class="flex items-center"
>
<input
id="filter-mid"
v-model="level"
value="50"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-mid"
class="ml-3 block text-sm font-medium text-gray-700"
>
중간 필터
</label>
</div>
<div
class="flex items-center"
>
<input
id="filter-low"
v-model="level"
value="100"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-low"
class="ml-3 block text-sm font-medium text-gray-700"
>
가장 가벼운 필터
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<label
for="memo"
class="block text-sm font-medium text-gray-700"
>
메모
</label>
<div class="mt-1">
<textarea
id="memo"
v-model="memo"
name="memo"
rows="3"
class="shadow-sm focus:ring-sky-500 focus:border-sky-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md"
/>
</div>
<p class="mt-2 text-sm text-gray-500"></p>
</div>
</div>
</div>
</div>
<!-- Privacy section -->
<div class="pt-0">
<div class="mt-4 py-4 px-4 flex justify-between sm:px-6">
<div>
<button
type="button"
class="bg-red-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
@click="doDelete"
>
{{ status == 0 ? '삭제' : '복구' }}
</button>
</div>
<div>
<button
type="button"
class="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500"
@click="doCancel"
>
취소
</button>
<button
type="button"
class="ml-5 bg-sky-700 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-sky-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500"
@click="doUpdateInfo"
>
저장
</button>
</div>
</div>
</div>
</form>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'check-auth-admin',
});
const route = useRoute();
const router = useRouter();
const hero = route.params.hero;
function gotoAdminKeyLog() {
navigateTo('/admin/word/' + hero + '/log');
}
const by = ref('');
const raw = ref('');
const level = ref('');
const memo = ref('');
const status = ref(0);
const responseJson = await _crossCtl.doComm('local/select', 'word', {
hero: hero,
});
if (responseJson['responseMessage'] == 'ok') {
console.log(responseJson['data']);
raw.value = responseJson['data'][0]['raw'];
level.value = responseJson['data'][0]['level'];
memo.value = responseJson['data'][0]['memo'];
by.value = responseJson['data'][0]['by'];
status.value = responseJson['data'][0]['status'];
} else {
alert(responseJson['responseMessage']);
}
async function doUpdateInfo() {
if (inPregressFlag.value == true) {
alert('이전 동작이 아직 진행중입니다.');
return;
}
inPregressFlag.value = true;
const responseJson = await _crossCtl.doComm('local/update', 'word', {
hero: hero,
raw: raw.value,
level: level.value,
memo: memo.value,
status: status.value,
});
inPregressFlag.value = false;
if (responseJson['responseMessage'] == 'ok') {
alert('ok');
} else {
alert(responseJson['responseMessage']);
}
}
async function doCancel() {
router.back();
}
const inPregressFlag = ref(false);
async function doDelete() {
if (inPregressFlag.value == true) {
alert('이전 동작이 아직 진행중입니다.');
return;
}
if (
window.confirm(
status.value == 0
? '이 표현을 삭제하시겠습니까?'
: '이 표현을 복구하시겠습니까?'
)
) {
inPregressFlag.value = true;
if (status.value == 0) {
const responseJson = await _crossCtl.doComm(
'local/delete',
'word',
{
hero: hero,
raw: raw.value,
}
);
inPregressFlag.value = false;
if (responseJson['responseMessage'] == 'ok') {
status.value = 4;
alert('ok');
} else {
alert(responseJson['responseMessage']);
}
} else {
status.value = 0;
const responseJson = await _crossCtl.doComm(
'local/update',
'word',
{
hero: hero,
raw: raw.value,
level: level.value,
memo: memo.value,
status: 0,
revive: true,
}
);
inPregressFlag.value = false;
if (responseJson['responseMessage'] == 'ok') {
status.value = 0;
alert('ok');
// router.back();
} else {
alert(responseJson['responseMessage']);
}
}
}
}
</script>

View File

@@ -0,0 +1,328 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="divide-y divide-gray-200">
<form class="lg:col-span-9">
<!-- Profile section -->
<div class="py-6 px-4 sm:p-6 lg:pb-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
필터 단어 정보 확인, 변경
</h1>
<p class="mt-2 text-sm text-gray-700">
필터 단어의 세부 내용을 확인하고 수정합니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<img
v-if="inPregressFlag"
width="32"
src="/loading-load-2.gif"
/>
</div>
</div>
<div class="mt-6 grid grid-cols-12 gap-6">
<div class="col-span-12 sm:col-span-6">
<label
for="by"
class="block text-sm font-medium text-gray-700"
>등록자</label
>
<input
id="by"
v-model="by"
disabled
type="text"
name="by"
autocomplete="by"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
/>
</div>
<div class="col-span-12 sm:col-span-6"></div>
</div>
<div class="mt-6 flex flex-col lg:flex-row">
<div class="flex-grow space-y-6">
<div>
<label
for="raw"
class="block text-sm font-medium text-gray-700"
>
모욕적인 표현
</label>
<div class="mt-1 rounded-md shadow-sm flex">
<input
id="raw"
v-model="raw"
disabled
type="text"
name="raw"
autocomplete="raw"
class="focus:ring-sky-500 focus:border-sky-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"
/>
</div>
</div>
<div
class="space-y-6 sm:space-y-5 divide-y divide-gray-200"
>
<div class="pt-6 sm:pt-5">
<div
role="group"
aria-labelledby="label-notifications"
>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline"
>
<div>
<div
id="label-notifications"
class="text-base font-medium text-gray-900 sm:text-sm sm:text-gray-700"
>
필터 단계
</div>
</div>
<div class="sm:col-span-2">
<div class="max-w-lg">
<p
class="text-sm text-gray-500"
>
필터 대상이 되는 단계를
선택합니다. (현재는 최대
필터만 동작)
</p>
<div class="mt-4 space-y-4">
<div
class="flex items-center"
>
<input
id="filter-high"
v-model="level"
value="10"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-high"
class="ml-3 block text-sm font-medium text-gray-700"
>
최대 필터
</label>
</div>
<div
class="flex items-center"
>
<input
id="filter-mid"
v-model="level"
value="50"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-mid"
class="ml-3 block text-sm font-medium text-gray-700"
>
중간 필터
</label>
</div>
<div
class="flex items-center"
>
<input
id="filter-low"
v-model="level"
value="100"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-low"
class="ml-3 block text-sm font-medium text-gray-700"
>
가장 가벼운 필터
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<label
for="memo"
class="block text-sm font-medium text-gray-700"
>
메모
</label>
<div class="mt-1">
<textarea
id="memo"
v-model="memo"
name="memo"
rows="3"
class="shadow-sm focus:ring-sky-500 focus:border-sky-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md"
/>
</div>
<p class="mt-2 text-sm text-gray-500"></p>
</div>
</div>
</div>
</div>
<!-- Privacy section -->
<div class="pt-0">
<div class="mt-4 py-4 px-4 flex justify-between sm:px-6">
<div>
<button
type="button"
class="bg-red-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
@click="doDelete"
>
{{ status == 0 ? '삭제' : '복구' }}
</button>
</div>
<div>
<button
type="button"
class="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500"
@click="doCancel"
>
취소
</button>
<button
type="button"
class="ml-5 bg-sky-700 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-sky-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500"
@click="doUpdateInfo"
>
저장
</button>
</div>
</div>
</div>
</form>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'check-auth-admin',
});
const route = useRoute();
const router = useRouter();
const hero = route.params.target;
const by = ref('');
const raw = ref('');
const level = ref('');
const memo = ref('');
const status = ref(0);
const responseJson = await _crossCtl.doComm('local/select', 'word', {
hero: hero,
});
if (responseJson['responseMessage'] == 'ok') {
console.log(responseJson['data']);
raw.value = responseJson['data'][0]['raw'];
level.value = responseJson['data'][0]['level'];
memo.value = responseJson['data'][0]['memo'];
by.value = responseJson['data'][0]['by'];
status.value = responseJson['data'][0]['status'];
} else {
alert(responseJson['responseMessage']);
}
async function doUpdateInfo() {
if (inPregressFlag.value == true) {
alert('이전 동작이 아직 진행중입니다.');
return;
}
inPregressFlag.value = true;
const responseJson = await _crossCtl.doComm('local/update', 'word', {
hero: hero,
raw: raw.value,
level: level.value,
memo: memo.value,
status: status.value,
});
inPregressFlag.value = false;
if (responseJson['responseMessage'] == 'ok') {
alert('ok');
} else {
alert(responseJson['responseMessage']);
}
}
async function doCancel() {
router.back();
}
const inPregressFlag = ref(false);
async function doDelete() {
if (inPregressFlag.value == true) {
alert('이전 동작이 아직 진행중입니다.');
return;
}
if (
window.confirm(
status.value == 0
? '이 표현을 삭제하시겠습니까?'
: '이 표현을 복구하시겠습니까?'
)
) {
inPregressFlag.value = true;
if (status.value == 0) {
const responseJson = await _crossCtl.doComm(
'local/delete',
'word',
{
hero: hero,
raw: raw.value,
}
);
inPregressFlag.value = false;
if (responseJson['responseMessage'] == 'ok') {
status.value = 4;
alert('ok');
} else {
alert(responseJson['responseMessage']);
}
} else {
status.value = 0;
const responseJson = await _crossCtl.doComm(
'local/update',
'word',
{
hero: hero,
raw: raw.value,
level: level.value,
memo: memo.value,
status: 0,
revive: true,
}
);
inPregressFlag.value = false;
if (responseJson['responseMessage'] == 'ok') {
status.value = 0;
alert('ok');
// router.back();
} else {
alert(responseJson['responseMessage']);
}
}
}
}
</script>

View File

@@ -0,0 +1,467 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<br />
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
{{ pageTitle }}
</h1>
<p class="mt-2 text-sm text-gray-700">
전체 등록 필터 단어 리스트를 확인할 있습니다.
</p>
</div>
<button
v-for="(headingAction, index) in headingActions"
:key="headingAction"
type="button"
:class="index > 0 ? 'ml-3' : ''"
class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-3 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
@click="doHeadingAction(headingAction)"
>
{{ headingAction }}
</button>
<div class="ml-3">
<select
id="targetLevel"
v-model="targetLevel"
name="targetLevel"
class="block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
@change="onChangeLevel($event)"
>
<option>all</option>
<option>high</option>
<option>mid</option>
<option>low</option>
</select>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<label for="mobile-search-candidate" class="sr-only"
>Search</label
>
<label for="desktop-search-candidate" class="sr-only"
>Search</label
>
<div class="flex rounded-md shadow-sm">
<div class="relative flex-grow focus-within:z-10">
<div
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
>
<img
v-if="inPregressFlag"
width="22"
src="/loading-load-2.gif"
/>
</div>
<input
id="mobile-search-candidate"
v-model="searchKeyword"
type="text"
name="mobile-search-candidate"
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 pr-10 sm:hidden border-gray-300"
placeholder=""
@keydown.enter.prevent="onEnterHandler()"
/>
<input
id="desktop-search-candidate"
v-model="searchKeyword"
type="text"
name="desktop-search-candidate"
class="hidden focus:ring-indigo-500 focus:border-indigo-500 w-full rounded-none rounded-l-md pl-10 pr-10 sm:block sm:text-sm border-gray-300"
placeholder=""
@keydown.enter.prevent="onEnterHandler()"
/>
<div
class="absolute inset-y-0 right-0 pr-3 lr-3 flex items-center"
>
<a href="javascript:void(0)" @click="clearSearch()">
<XCircleIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</a>
</div>
</div>
<button
type="button"
class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-r-md text-gray-700 bg-gray-50 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
@click="doAction('search', searchKeyword)"
>
<span class="ml-2">검색</span>
</button>
</div>
</div>
</div>
<div class="mt-3"></div>
<div v-if="searchKeyword != ''">
검색 대상 단어 : {{ searchKeyword }}
<div class="text-red-600 text-sm">
{{
searchKeyword.trim() != searchKeyword
? '주의! 검색어 앞이나 뒤에 공백이 포함되어 있습니다!'
: ''
}}
</div>
</div>
<div class="mt-3"></div>
<div v-if="searchKeyword != ''">정확한 검색 결과</div>
<BaseList1
v-if="searchKeyword != ''"
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="exactMatchs"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<div class="mt-3"></div>
<div v-if="searchKeyword != ''">유사한 검색 결과</div>
<BaseList1
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="listData"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<BasePagination1
:total-page-count="totalPageCount"
:current-page-number="currentPageNumber"
:page-size="pageSize"
:records-total="recordsTotal"
:page-move="pageMove"
/>
<div class="p-0 rounded-bl-2xl rounded-br-2xl md:px-0">
<a
href="javascript:void(0)"
class="text-base font-medium text-indigo-700 hover:text-indigo-600"
@click="$router.back()"
>&larr;이전 화면으로<span aria-hidden="true"> </span
></a>
</div>
</div>
</template>
<script setup lang="ts">
import {
ChevronDownIcon,
MagnifyingGlassCircleIcon,
XCircleIcon,
} from '@heroicons/vue/24/solid';
const { $dayjs } = useNuxtApp();
const route = useRoute();
const inPregressFlag = ref(false);
const listMode = ref(route.query.mode ? route.query.mode : '');
const pageTitle = ref(
listMode.value == 'trashcan'
? '필터 단어 - 삭제 단어 리스트'
: '필터 단어 - 리스트'
);
const router = useRouter();
definePageMeta({
middleware: 'check-auth-admin',
});
const headingActions = ['표현 추가', '리스트 모드'];
let listTarget = 'admin:word:all';
let hero = '';
const searchKeyword = ref('');
const targetLevel = ref('all');
const listHeadings = [
{
title: '일련번호',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'serial',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass:
'w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6',
},
{
title: '구분',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
key: 'tag',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
},
{
title: '단어',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
key: 'raw',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
},
{
title: '레벨',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'level',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: '상태',
class: 'px-3 py-3.5 text-left text-sm font-semibold text-gray-900',
key: 'status',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'px-3 py-4 text-sm text-gray-500',
},
{
title: '수정일',
class: 'px-3 py-3.5 text-left text-sm font-semibold text-gray-900',
key: 'updated',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'px-3 py-4 text-sm text-gray-500',
},
];
const listActions = ['상세보기'];
const actionKey = 'serial';
const listKeys = [
'serial',
'raw',
'level',
'comment',
'status',
'updated',
'created',
];
const listData = ref([]);
const exactMatchs = ref([]);
const totalPageCount = ref(0);
const currentPageNumber = ref(1);
/*
if (_crossCtl.volatilePool['/admin/filter/word/list:hero'] != undefined) {
console.log('got cache');
hero = _crossCtl.volatilePool['/admin/filter/word/list:hero'];
searchKeyword.value = hero;
listTarget = 'admin:word:like';
_crossCtl.volatilePool['/admin/filter/word/list:hero'] = undefined;
} else {
console.log('no cache');
}
if (
_crossCtl.volatilePool['/admin/filter/word/list:currentPageNumber'] !=
undefined
) {
console.log('got page cache');
currentPageNumber.value =
_crossCtl.volatilePool['/admin/filter/word/list:currentPageNumber'];
_crossCtl.volatilePool['/admin/filter/word/list:currentPageNumber'] =
undefined;
} else {
console.log('no page cache');
}
*/
async function doHeadingAction(tag) {
console.log('on doHeadingAction(), tag=', tag);
switch (tag) {
case '표현 추가':
navigateTo('/admin/filter/word/new');
break;
case '리스트 모드':
console.log('listMode.value=', '[' + listMode.value + ']');
if (listMode.value == 'trashcan') {
console.log('huk 1');
pageTitle.value = '필터 단어 - 리스트';
await navigateTo('/admin/filter/word/list', { replace: true });
listMode.value = '';
} else {
console.log('huk 2');
pageTitle.value = '필터 단어 - 삭제 리스트';
await navigateTo('/admin/filter/word/list?mode=trashcan', {
replace: true,
});
listMode.value = 'trashcan';
}
pageMove(1);
break;
default:
alert('unhandled heading action. tag = ' + tag);
}
// alert('headingAction : ' + tag);
}
const pageSize = ref(10);
const recordsTotal = ref(0);
// const order = [{ column: 'serial', dir: 'desc' }];
// const columns = { serial: { data: 'serial' } };
function columnFilter(key, val) {
// console.log("columnFilter(), key = ", key, ", val = ", val);
if (key == 'updated' || key == 'created') {
// return $dayjs(val).format('YY/MM/DD A h:mm:ss');
return $dayjs(val).format('YY/MM/DD');
} else if (key == 'raw') {
return '[' + val + ']';
} else if (key == 'status') {
let statusTag = '정상';
switch (val) {
case 0:
statusTag = '정상등록';
break;
case 4:
statusTag = '삭제됨';
break;
default:
statusTag = val;
}
return statusTag;
} else if (key == 'level') {
let levelTag = 'mid';
switch (val) {
case 10:
levelTag = 'high';
break;
case 50:
levelTag = 'mid';
break;
case 100:
levelTag = 'low';
break;
default:
levelTag = val;
}
return levelTag;
} else {
return val;
}
}
function doAction(tag, target) {
console.log('on doAction(), tag=', tag, ', target=', target);
if (tag == '상세보기') {
navigateTo('/admin/filter/word/' + target + '/edit');
} else if (tag == 'search') {
console.log('search for ', target);
if (target == '') {
listTarget = 'admin:word:all';
} else {
listTarget = 'admin:word:like';
hero = target;
}
refresh();
}
}
function pageMove(targetPageIdex) {
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function refresh() {
if (process.client) {
inPregressFlag.value = true;
const tmpListTarget =
listTarget + (listMode.value == 'trashcan' ? ':deleted' : '');
console.log('tmpListTarget=', tmpListTarget);
const responseJson = await _crossCtl.doComm(
'local/list',
tmpListTarget,
{
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
hero: hero,
targetLevel: targetLevel.value,
}
);
console.log('responseJson=', responseJson);
inPregressFlag.value = false;
if (responseJson['responseCode'] == 200) {
currentPageNumber.value = responseJson['currentPageNumber'];
totalPageCount.value = responseJson['totalPageCount'];
pageSize.value = parseInt(responseJson['pageSize']);
recordsTotal.value = responseJson['recordsTotal'];
listData.value = responseJson['data'];
if (responseJson['metaData'] != null) {
exactMatchs.value = responseJson['metaData']['exactMatchs'];
}
} else {
alert(responseJson['responseMessage']);
}
}
}
function onChangeLevel(e) {
console.log('targetLevel.value=', targetLevel.value);
refresh();
}
function onEnterHandler() {
currentPageNumber.value = 1;
doAction('search', searchKeyword.value);
}
function clearSearch() {
exactMatchs.value = [];
currentPageNumber.value = 1;
searchKeyword.value = '';
doAction('search', searchKeyword.value);
}
refresh();
</script>

View File

@@ -0,0 +1,257 @@
<!--
This example requires Tailwind CSS v2.0+
This example requires some changes to your config:
```
// tailwind.config.js
module.exports = {
// ...
plugins: [
// ...
require('@tailwindcss/forms'),
],
}
```
-->
<template>
<div class="m-8">
<form
class="space-y-8 divide-y divide-gray-200"
@submit.prevent="doCreate"
>
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
<div
class="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5"
>
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
욕설·비속어 등록
</h1>
<p class="mt-2 text-sm text-gray-700">
필터 대상이 욕설·비속어을 추가로 등록 합니다.
현재는 개발 테스트용으로 단어 하나 추가
완료까지 시간이 오래 걸립니다. 단어 추가 바로
수정된 내용을 연속으로 추가할 있습니다.
작업을 마치고 나면 이전 화면으로 돌아가는 버튼을
누르세요.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<img
v-if="inPregressFlag"
width="32"
src="/loading-load-2.gif"
/>
</div>
</div>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5"
>
<label
for="raw"
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
표현
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<input
id="raw"
v-model="raw"
type="text"
name="raw"
autocomplete="raw"
class="max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</div>
</div>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5"
>
<label
for="memo"
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
메모
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<textarea
id="memo"
v-model="memo"
name="memo"
rows="3"
class="shadow-sm focus:ring-sky-500 focus:border-sky-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md"
/>
</div>
</div>
<div
class="space-y-6 sm:space-y-5 divide-y divide-gray-200"
>
<div class="pt-6 sm:pt-5">
<div
role="group"
aria-labelledby="label-notifications"
>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline"
>
<div>
<div
id="label-notifications"
class="text-base font-medium text-gray-900 sm:text-sm sm:text-gray-700"
>
필터 단계
</div>
</div>
<div class="sm:col-span-2">
<div class="max-w-lg">
<p class="text-sm text-gray-500">
필터 대상이 되는 단계를
선택합니다. (현재는 최대 필터만
동작)
</p>
<div class="mt-4 space-y-4">
<div class="flex items-center">
<input
id="filter-high"
v-model="level"
value="10"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-high"
class="ml-3 block text-sm font-medium text-gray-700"
>
최대 필터
</label>
</div>
<div class="flex items-center">
<input
id="filter-mid"
v-model="level"
value="50"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-mid"
class="ml-3 block text-sm font-medium text-gray-700"
>
중간 필터
</label>
</div>
<div class="flex items-center">
<input
id="filter-low"
v-model="level"
value="100"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-low"
class="ml-3 block text-sm font-medium text-gray-700"
>
가장 가벼운 필터
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="pt-5">
<div class="flex justify-end">
<button
type="button"
class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="doCancel"
>
이전 화면으로 이동
</button>
<button
type="submit"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
만들기
</button>
</div>
</div>
</form>
</div>
</template>
<script setup lang="ts">
const router = useRouter();
definePageMeta({
middleware: 'check-auth-admin',
});
const raw = ref('');
const memo = ref('');
const level = ref(10);
async function doCancel() {
if (inPregressFlag.value == true) {
alert('이전 동작이 아직 진행중입니다.');
return;
}
router.back();
}
watch(level, (newValue, oldValue) => {
console.log('level 의 변이가 감지되었을 때 ', {
newValue,
oldValue,
});
});
const inPregressFlag = ref(false);
async function doCreate() {
if (inPregressFlag.value == true) {
alert('이전 동작이 아직 진행중입니다.');
return;
}
if (raw.value == '') {
alert('빈칸은 입력하실 수 없습니다.');
return;
}
inPregressFlag.value = true;
const responseJson = await _crossCtl.doComm('local/insert', 'word', {
raw: raw.value,
memo: memo.value,
level: level.value,
});
inPregressFlag.value = false;
if (responseJson['responseMessage'] == 'ok') {
alert('ok');
} else {
if (responseJson['responseMessage'] == 'ER_DUP_ENTRY') {
alert(
'오류 : 이미 같은 단어가 등록되어 있습니다. 삭제된 상태의 단어를 다시 등록하시려는 경우에는 복구 기능을 이용해 주세요. '
);
} else {
alert('오류 : ' + responseJson['responseMessage']);
}
}
}
</script>

View File

@@ -0,0 +1,318 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
API 사용량 통계 상세 보기
</h1>
<p class="mt-2 text-sm text-gray-700">
API 키별 사용량 통계를 보여 줍니다. 구분 항목으로 시간별,
날짜별, 월별 구분이 가능합니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<div>
<label
for="location"
class="block text-sm font-medium text-gray-700"
>API Key</label
>
<select
id="targetKey"
v-model="targetKey"
name="targetKey"
class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
@change="onChange($event)"
>
<option
v-for="item in keys"
:key="item.key"
:selected="item.current"
:value="item.key"
>
<span class="truncate">
{{ item.name }}
</span>
</option>
</select>
</div>
</div>
</div>
<BaseList1
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="listData"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<BasePagination1
:total-page-count="totalPageCount"
:current-page-number="currentPageNumber"
:page-size="pageSize"
:records-total="recordsTotal"
:page-move="pageMove"
/>
<div class="p-0 rounded-bl-2xl rounded-br-2xl md:px-0">
<a
href="javascript:void(0)"
class="text-base font-medium text-indigo-700 hover:text-indigo-600"
@click="$router.back()"
>&larr;이전 화면으로<span aria-hidden="true"> </span
></a>
</div>
</div>
</template>
<script setup lang="ts">
import HeroVue from '~~/base/pages/support/inquiry/view/[hero].vue';
const router = useRouter();
definePageMeta({
middleware: 'check-auth-op',
});
const listHeadings = [
{
title: '구분',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'date_tag',
hiddenInfo: {
headClass: 'font-normal lg:hidden',
dts: [],
dds: [],
},
subClass:
'w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6',
},
{
title: 'total',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
key: 'total',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
},
{
title: 'hit',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'hit',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'hit_ratio',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'hit_ratio',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'size',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'size',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'size_avg',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'size_avg',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'ip',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'uniq_ip',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'referrer',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'uniq_referrer',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: '갱신일',
class: 'px-3 py-3.5 text-left text-sm font-semibold text-gray-900',
key: 'updated',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'px-3 py-4 text-sm text-gray-500',
},
];
const listActions = [];
const actionKey = 'serial';
const listKeys = [
'serial',
'date_tag',
'total',
'hit',
'size',
'uniq_ip',
'uniq_referrer',
'updated',
];
const listData = ref([]);
const totalPageCount = ref(0);
const currentPageNumber = ref(1);
const pageSize = ref(26);
const recordsTotal = ref(0);
// const order = [{ column: 'serial', dir: 'desc' }];
// const columns = { serial: { data: 'serial' } };
const { $dayjs } = useNuxtApp();
function columnFilter(key, val) {
// console.log('columnFilter(), key = ', key, ', val = ', val);
if (key == 'updated' || key == 'created') {
return $dayjs(val).format('YY/MM/DD A h:mm:ss');
// return $dayjs(val).format('YY/MM/DD');
} else if (key == 'size') {
return _utils.formatBytes(val, 2);
} else {
return val;
}
}
function doAction(tag, target) {
console.log('on doAction(), tag=', tag, ', target=', target);
router.push({
name: 'key-edit',
params: { target: target },
});
}
function pageMove(targetPageIdex) {
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function refresh() {
const responseJson = await _crossCtl.doComm(
'local/list',
'admin:statistics',
{
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
hero: targetKey.value,
term: _term,
termPrefix: '2022',
}
);
currentPageNumber.value = responseJson['currentPageNumber'];
totalPageCount.value = responseJson['totalPageCount'];
pageSize.value = parseInt(responseJson['pageSize']);
recordsTotal.value = responseJson['recordsTotal'];
for (let i = 0; i < responseJson['data'].length; i++) {
responseJson['data'][i]['hit_ratio'] =
(
(responseJson['data'][i]['hit'] /
responseJson['data'][i]['total']) *
100
).toFixed(2) + '%';
responseJson['data'][i]['size_avg'] = _utils.formatBytes(
responseJson['data'][i]['size'] / responseJson['data'][i]['total'],
2
);
}
listData.value = responseJson['data'];
console.log('listData.value=', listData.value);
}
const targetKey = ref('');
const keys = ref([]);
const route = useRoute();
const hero = route.params.hero;
const _term = route.params._term;
console.log('hero = ', hero);
console.log('_term = ', _term);
const responseJson = await _crossCtl.doComm('local/select', 'admin:key', {
hero: hero,
});
if (responseJson['data'].length == 0) {
_crossCtl.openModal(
'error',
'잘못된 파라메타',
'키 정보를 읽어올 수 없습니다.\n확인 버튼을 누르면 메인 화면으로 돌아갑니다.',
['확인'],
(btnIdx) => {
navigateTo('/');
}
);
} else {
const tmpKeys = [];
for (let i = 0; i < responseJson['data'].length; i++) {
if (i == 0) {
targetKey.value = responseJson['data'][i]['api_key'];
}
tmpKeys.push({
current: i == 0,
name: responseJson['data'][i]['name'],
key: responseJson['data'][i]['api_key'],
});
}
keys.value = tmpKeys;
}
function onChange(e) {
console.log('targetKey.value=', targetKey.value);
refresh();
}
refresh();
</script>

View File

@@ -0,0 +1,439 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
API 사용량 통계 상세 보기
</h1>
<p class="mt-2 text-sm text-gray-700">
API 키별 사용량 통계를 보여 줍니다. 구분 항목으로 시간별,
날짜별, 월별 구분이 가능합니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<div>
<select
id="targetTerm"
v-model="targetTerm"
name="targetTerm"
class="mt-0 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
@change="onChange($event)"
>
<option
v-for="term in terms"
:key="term.key"
:selected="term.current"
:value="term.key"
>
<span class="truncate">
{{ term.key }}
</span>
</option>
</select>
</div>
</div>
<Datepicker
v-if="targetTerm == 'month'"
v-model="targetDateMonth"
class="mt-4 sm:mt-0 sm:ml-2 sm:flex-none"
locale="ko"
:year-picker="true"
:format="formatForMonth"
@update:modelValue="handleDate"
></Datepicker>
<Datepicker
v-if="targetTerm == 'day'"
v-model="targetDateDay"
class="mt-4 sm:mt-0 sm:ml-2 sm:flex-none"
locale="ko"
:month-picker="true"
:format="formatForDay"
@update:modelValue="handleDate"
></Datepicker>
<Datepicker
v-if="targetTerm == 'hour'"
v-model="targetDateHour"
class="mt-4 sm:mt-0 sm:ml-2 sm:flex-none"
locale="ko"
:format="formatForHour"
@update:modelValue="handleDate"
></Datepicker>
</div>
<BaseList1
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="listData"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<BasePagination1
:total-page-count="totalPageCount"
:current-page-number="currentPageNumber"
:page-size="pageSize"
:records-total="recordsTotal"
:page-move="pageMove"
/>
<div class="p-0 rounded-bl-2xl rounded-br-2xl md:px-0">
<a
href="javascript:void(0)"
class="text-base font-medium text-indigo-700 hover:text-indigo-600"
@click="$router.back()"
>&larr;이전 화면으로<span aria-hidden="true"> </span
></a>
</div>
</div>
</template>
<script setup lang="ts">
import Datepicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
const { $dayjs } = useNuxtApp();
const router = useRouter();
const targetDate = ref($dayjs(new Date().toISOString()).format('YYYY'));
const targetDateMonth = ref($dayjs(new Date().toISOString()).format('YYYY'));
const targetDateDay = ref({
month: new Date().getMonth(),
year: new Date().getFullYear,
});
const targetDateHour = ref(
$dayjs(new Date().toISOString()).format('DD/MM/YYYY')
);
// console.log('huk = ', targetDate.value);
const formatForMonth = (date) => {
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
// return `${year}${(month < 10 ? '0' : '') + month}${(day < 10 ? '0' : '') + day}`;
return `${year}`;
};
const formatForDay = (date) => {
console.log('huk format date = ', date);
// return `${year}${(month < 10 ? '0' : '') + month}${(day < 10 ? '0' : '') + day}`;
return `${date.year}${(date.month + 1 < 10 ? '0' : '') + (date.month + 1)}`;
};
const formatForHour = (date) => {
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
const hour = date.getHours();
return `${year}${(month < 10 ? '0' : '') + month}${
(day < 10 ? '0' : '') + day
}`;
};
definePageMeta({
middleware: 'check-auth-op',
});
const listHeadings = [
{
title: '구분',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'date_tag',
hiddenInfo: {
headClass: 'font-normal lg:hidden',
dts: [],
dds: [],
},
subClass:
'w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6',
},
{
title: 'total',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
key: 'total',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
},
{
title: 'hit',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'hit',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'hit_ratio',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'hit_ratio',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'size',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'size',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'size_avg',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'size_avg',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'ip',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'uniq_ip',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'referrer',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'uniq_referrer',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: '갱신일',
class: 'px-3 py-3.5 text-left text-sm font-semibold text-gray-900',
key: 'updated',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'px-3 py-4 text-sm text-gray-500',
},
];
const listActions = [];
const actionKey = 'serial';
const listKeys = [
'serial',
'date_tag',
'total',
'hit',
'size',
'uniq_ip',
'uniq_referrer',
'updated',
];
const listData = ref([]);
const totalPageCount = ref(0);
const currentPageNumber = ref(1);
const pageSize = ref(26);
const recordsTotal = ref(0);
// const order = [{ column: 'serial', dir: 'desc' }];
// const columns = { serial: { data: 'serial' } };
function columnFilter(key, val) {
// console.log('columnFilter(), key = ', key, ', val = ', val);
if (key == 'updated' || key == 'created') {
return $dayjs(val).format('YY/MM/DD A h:mm:ss');
// return $dayjs(val).format('YY/MM/DD');
} else if (key == 'size') {
return _utils.formatBytes(val, 2);
} else {
return val;
}
}
function doAction(tag, target) {
console.log('on doAction(), tag=', tag, ', target=', target);
router.push({
name: 'key-edit',
params: { target: target },
});
}
function pageMove(targetPageIdex) {
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function refresh() {
const responseJson = await _crossCtl.doComm(
'local/list',
'admin:statistics',
{
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
hero: targetKey.value,
term: targetTerm.value,
// termPrefix: _utils.getDateTimeTag(targetTerm.value.substring(0, 1)),
// termPrefix: _utils.getDateTimeTag('y'),
termPrefix: targetDate.value,
}
);
currentPageNumber.value = responseJson['currentPageNumber'];
totalPageCount.value = responseJson['totalPageCount'];
pageSize.value = parseInt(responseJson['pageSize']);
recordsTotal.value = responseJson['recordsTotal'];
for (let i = 0; i < responseJson['data'].length; i++) {
responseJson['data'][i]['hit_ratio'] =
(
(responseJson['data'][i]['hit'] /
responseJson['data'][i]['total']) *
100
).toFixed(2) + '%';
responseJson['data'][i]['size_avg'] = _utils.formatBytes(
responseJson['data'][i]['size'] / responseJson['data'][i]['total'],
2
);
}
listData.value = responseJson['data'];
console.log('listData.value=', listData.value);
}
const terms = ref([]);
const route = useRoute();
const hero = route.params.hero;
const targetTerm = ref('year');
const targetKey = ref('');
const tmpTerms = [
{ current: targetTerm.value == 'year', key: 'year' },
{ current: targetTerm.value == 'month', key: 'month' },
{ current: targetTerm.value == 'day', key: 'day' },
{ current: targetTerm.value == 'hour', key: 'hour' },
];
terms.value = tmpTerms;
const responseJson = await _crossCtl.doComm('local/select', 'admin:key', {
hero: hero,
});
if (responseJson['data'].length == 0) {
_crossCtl.openModal(
'error',
'잘못된 파라메타',
'키 정보를 읽어올 수 없습니다.\n확인 버튼을 누르면 메인 화면으로 돌아갑니다.',
['확인'],
(btnIdx) => {
navigateTo('/');
}
);
} else {
targetKey.value = responseJson['data'][0]['api_key'];
}
function handleDate(date) {
console.log('huk date = ', date);
let result = date;
switch (targetTerm.value) {
case 'year':
result = `${date}`;
break;
case 'month':
result = `${date}`;
break;
case 'day':
result = `${date.year}${
(date.month + 1 < 10 ? '0' : '') + (date.month + 1)
}`;
break;
case 'hour':
result = $dayjs(date).format('YYYYMMDD');
break;
}
targetDate.value = result;
console.log('huk result = ', result);
refresh();
}
function onChange(e) {
console.log('targetTerm.value=', targetTerm.value);
switch (targetTerm.value) {
case 'year':
targetDate.value = $dayjs(new Date().toISOString()).format('YYYY');
break;
case 'month':
targetDate.value = $dayjs(new Date().toISOString()).format('YYYY');
break;
case 'day':
targetDate.value = $dayjs(new Date().toISOString()).format(
'YYYYMM'
);
break;
case 'hour':
targetDate.value = $dayjs(new Date().toISOString()).format(
'YYYYMMDD'
);
break;
}
terms.value = [
{ current: targetTerm.value == 'year', key: 'year' },
{ current: targetTerm.value == 'month', key: 'month' },
{ current: targetTerm.value == 'day', key: 'day' },
{ current: targetTerm.value == 'hour', key: 'hour' },
];
refresh();
}
refresh();
</script>

View File

@@ -0,0 +1,327 @@
<template>
<div class="m-8">
<form
class="space-y-8 divide-y divide-gray-200"
@submit.prevent="doUpdate"
>
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
<div
class="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5"
>
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
API 상세 보기
</h1>
<p class="mt-2 text-sm text-gray-700">
API키의 설정을 변경하거나 삭제할 있습니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button
type="button"
class="mr-2 inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"
@click="
navigateTo(
'/admin/statistics/byterm/word?key=' +
apiKey
)
"
>
단어 통계
</button>
<button
type="button"
class="mr-2 inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"
@click="
navigateTo(
'/admin/statistics/byterm/usage?key=' +
apiKey
)
"
>
사용 통계
</button>
<button
type="button"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="gotoAdminKeyLog()"
>
로그
</button>
<button
type="button"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="gotoAdminStatistics()"
>
사용량 통계
</button>
<button
type="button"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="gotoAdminWordStatistics()"
>
단어 통계
</button>
</div>
</div>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5"
>
<label
for="name"
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
이름
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<input
id="name"
v-model="name"
type="text"
name="name"
autocomplete="name"
class="max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</div>
</div>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5"
>
<label
for="api-key"
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
API
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<input
id="api-key"
v-model="apiKey"
type="text"
name="api-key"
autocomplete="api-key"
disabled
class="max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</div>
</div>
<div
class="space-y-6 sm:space-y-5 divide-y divide-gray-200"
>
<div class="pt-6 sm:pt-5">
<div
role="group"
aria-labelledby="label-notifications"
>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline"
>
<div>
<div
id="label-notifications"
class="text-base font-medium text-gray-900 sm:text-sm sm:text-gray-700"
>
필터 단계
</div>
</div>
<div class="sm:col-span-2">
<div class="max-w-lg">
<p class="text-sm text-gray-500">
필터 대상이 되는 단계를
선택합니다. (현재는 최대 필터만
동작)
</p>
<div class="mt-4 space-y-4">
<div class="flex items-center">
<input
id="filter-high"
v-model="level"
value="high"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-high"
class="ml-3 block text-sm font-medium text-gray-700"
>
최대 필터
</label>
</div>
<div class="flex items-center">
<input
id="filter-mid"
v-model="level"
value="mid"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-mid"
class="ml-3 block text-sm font-medium text-gray-700"
>
중간 필터
</label>
</div>
<div class="flex items-center">
<input
id="filter-low"
v-model="level"
value="low"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-low"
class="ml-3 block text-sm font-medium text-gray-700"
>
가장 가벼운 필터
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="pt-5">
<div class="flex justify-between">
<div>
<button
type="button"
class="bg-red-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
@click="doDelete"
>
{{ status == 0 ? '삭제' : '복구' }}
</button>
</div>
<div>
<button
type="button"
class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="doCancel"
>
이전화면
</button>
<button
v-if="status == 0"
type="submit"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
저장
</button>
</div>
</div>
</div>
</form>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const router = useRouter();
definePageMeta({
middleware: 'check-auth-admin',
});
const hero = route.params.hero;
function gotoAdminKeyLog() {
navigateTo('/admin/key/' + hero + '/log');
}
function gotoAdminStatistics() {
navigateTo('/admin/key/' + hero + '/statistics');
}
function gotoAdminWordStatistics() {
navigateTo('/admin/word/' + hero + '/statistics');
}
console.log('hero=', hero);
const apiKey = ref('');
const name = ref('');
const level = ref('');
const status = ref(0);
const responseJson = await _crossCtl.doComm('local/select', 'admin:key', {
hero: hero,
});
const keyInfo = responseJson['data'][0];
apiKey.value = keyInfo['api_key'];
name.value = keyInfo['name'];
level.value = keyInfo['level'];
status.value = keyInfo['status'];
async function doUpdate() {
const responseJson = await _crossCtl.doComm('local/update', 'admin:key', {
hero: hero,
name: name.value,
level: level.value,
status: status.value,
});
console.log('responseJson=', responseJson);
if (responseJson['responseMessage'] == 'ok') {
alert('ok');
} else {
alert(responseJson['responseMessage']);
}
}
async function doDelete() {
if (status.value == 0) {
const responseJson = await _crossCtl.doComm(
'local/delete',
'admin:key',
{
hero: hero,
}
);
console.log('responseJson=', responseJson);
if (responseJson['responseMessage'] == 'ok') {
status.value = 4;
alert('ok');
} else {
alert(responseJson['responseMessage']);
}
} else {
const responseJson = await _crossCtl.doComm(
'local/update',
'admin:key',
{
hero: hero,
name: name.value,
level: level.value,
revive: true,
status: 0,
}
);
console.log('responseJson=', responseJson);
if (responseJson['responseMessage'] == 'ok') {
status.value = 0;
alert('ok');
} else {
alert(responseJson['responseMessage']);
}
}
}
async function doCancel() {
router.back();
}
</script>

View File

@@ -0,0 +1,262 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<br />
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
API KEY 로그 보기
</h1>
<p class="mt-2 text-sm text-gray-700">
API KEY의 생성, 변경, 삭제 기록을 확인할 있습니다.
</p>
</div>
<!--
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<label for="mobile-search-candidate" class="sr-only"
>Search</label
>
<label for="desktop-search-candidate" class="sr-only"
>Search</label
>
<div class="flex rounded-md shadow-sm">
<div class="relative flex-grow focus-within:z-10">
<div
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
>
<MagnifyingGlassCircleIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</div>
<input
id="mobile-search-candidate"
v-model="searchKeyword"
type="text"
name="mobile-search-candidate"
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:hidden border-gray-300"
placeholder=""
/>
<input
id="desktop-search-candidate"
v-model="searchKeyword"
type="text"
name="desktop-search-candidate"
class="hidden focus:ring-indigo-500 focus:border-indigo-500 w-full rounded-none rounded-l-md pl-10 sm:block sm:text-sm border-gray-300"
placeholder=""
/>
</div>
<button
type="button"
class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-r-md text-gray-700 bg-gray-50 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
@click="doAction('search', searchKeyword)"
>
<span class="ml-2">검색</span>
</button>
</div>
</div>
-->
</div>
<BaseList1
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="listData"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<BasePagination1
:total-page-count="totalPageCount"
:current-page-number="currentPageNumber"
:page-size="pageSize"
:records-total="recordsTotal"
:page-move="pageMove"
/>
<div class="p-0 rounded-bl-2xl rounded-br-2xl md:px-0">
<a
href="javascript:void(0)"
class="text-base font-medium text-indigo-700 hover:text-indigo-600"
@click="$router.back()"
>&larr;이전 화면으로<span aria-hidden="true"> </span
></a>
</div>
</div>
</template>
<script setup lang="ts">
import {
ChevronDownIcon,
MagnifyingGlassCircleIcon,
} from '@heroicons/vue/24/solid';
import consolaGlobalInstance from 'consola';
const { $dayjs } = useNuxtApp();
const router = useRouter();
definePageMeta({
middleware: 'check-auth-admin',
});
const route = useRoute();
console.log('route.params=', route.params);
const targetName = route.params.hero;
const hero = route.params.hero;
console.log('targetName=', targetName);
console.log('hero=', hero);
const listTarget = 'admin:log:key';
const listHeadings = [
{
title: '누가',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'name',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass:
'w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6',
},
{
title: '언제',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
key: 'created',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
},
{
title: '무엇을',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'target_key',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: '어떻게',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'tag',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
];
const listActions = [];
const actionKey = 'serial';
const listKeys = [
'serial',
'raw',
'level',
'comment',
'status',
'updated',
'created',
];
const listData = ref([]);
const totalPageCount = ref(0);
const currentPageNumber = ref(1);
const pageSize = ref(10);
const recordsTotal = ref(0);
// const order = [{ column: 'serial', dir: 'desc' }];
// const columns = { serial: { data: 'serial' } };
function columnFilter(key, val) {
// console.log("columnFilter(), key = ", key, ", val = ", val);
if (key == 'updated' || key == 'created') {
return $dayjs(val).format('YY/MM/DD A h:mm:ss');
// return $dayjs(val).format('YY/MM/DD');
} else if (key == 'uid') {
return '' + targetName + '';
} else if (key == 'status') {
let statusTag = '정상';
switch (val) {
case 0:
statusTag = '정상등록';
break;
case 4:
statusTag = '삭제됨';
break;
default:
statusTag = val;
}
return statusTag;
} else if (key == 'level') {
let levelTag = 'mid';
switch (val) {
case 10:
levelTag = 'high';
break;
case 50:
levelTag = 'mid';
break;
case 100:
levelTag = 'low';
break;
default:
levelTag = val;
}
return levelTag;
} else {
return val;
}
}
function doAction(tag, target) {
console.log('on doAction(), tag=', tag, ', target=', target);
if (tag == '상세보기') {
navigateTo('/admin/user/' + hero + '/history/detail/' + target);
}
}
function pageMove(targetPageIdex) {
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function refresh() {
const responseJson = await _crossCtl.doComm('local/list', listTarget, {
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
hero: hero,
});
console.log('listTarget=', listTarget);
console.log('hero=', hero);
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'];
}
refresh();
</script>

View File

@@ -0,0 +1,439 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
API 사용량 통계 상세 보기
</h1>
<p class="mt-2 text-sm text-gray-700">
API 키별 사용량 통계를 보여 줍니다. 구분 항목으로 시간별,
날짜별, 월별 구분이 가능합니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<div>
<select
id="targetTerm"
v-model="targetTerm"
name="targetTerm"
class="mt-0 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
@change="onChange($event)"
>
<option
v-for="term in terms"
:key="term.key"
:selected="term.current"
:value="term.key"
>
<span class="truncate">
{{ term.key }}
</span>
</option>
</select>
</div>
</div>
<Datepicker
v-if="targetTerm == 'month'"
v-model="targetDateMonth"
class="mt-4 sm:mt-0 sm:ml-2 sm:flex-none"
locale="ko"
:year-picker="true"
:format="formatForMonth"
@update:modelValue="handleDate"
></Datepicker>
<Datepicker
v-if="targetTerm == 'day'"
v-model="targetDateDay"
class="mt-4 sm:mt-0 sm:ml-2 sm:flex-none"
locale="ko"
:month-picker="true"
:format="formatForDay"
@update:modelValue="handleDate"
></Datepicker>
<Datepicker
v-if="targetTerm == 'hour'"
v-model="targetDateHour"
class="mt-4 sm:mt-0 sm:ml-2 sm:flex-none"
locale="ko"
:format="formatForHour"
@update:modelValue="handleDate"
></Datepicker>
</div>
<BaseList1
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="listData"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<BasePagination1
:total-page-count="totalPageCount"
:current-page-number="currentPageNumber"
:page-size="pageSize"
:records-total="recordsTotal"
:page-move="pageMove"
/>
<div class="p-0 rounded-bl-2xl rounded-br-2xl md:px-0">
<a
href="javascript:void(0)"
class="text-base font-medium text-indigo-700 hover:text-indigo-600"
@click="$router.back()"
>&larr;이전 화면으로<span aria-hidden="true"> </span
></a>
</div>
</div>
</template>
<script setup lang="ts">
import Datepicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
const { $dayjs } = useNuxtApp();
const router = useRouter();
const targetDate = ref($dayjs(new Date().toISOString()).format('YYYY'));
const targetDateMonth = ref($dayjs(new Date().toISOString()).format('YYYY'));
const targetDateDay = ref({
month: new Date().getMonth(),
year: new Date().getFullYear,
});
const targetDateHour = ref(
$dayjs(new Date().toISOString()).format('MM/DD/YYYY')
);
// console.log('huk = ', targetDate.value);
const formatForMonth = (date) => {
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
// return `${year}${(month < 10 ? '0' : '') + month}${(day < 10 ? '0' : '') + day}`;
return `${year}`;
};
const formatForDay = (date) => {
console.log('huk format date = ', date);
// return `${year}${(month < 10 ? '0' : '') + month}${(day < 10 ? '0' : '') + day}`;
return `${date.year}${(date.month + 1 < 10 ? '0' : '') + (date.month + 1)}`;
};
const formatForHour = (date) => {
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
const hour = date.getHours();
return `${year}${(month < 10 ? '0' : '') + month}${
(day < 10 ? '0' : '') + day
}`;
};
definePageMeta({
middleware: 'check-auth-op',
});
const listHeadings = [
{
title: '구분',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'date_tag',
hiddenInfo: {
headClass: 'font-normal lg:hidden',
dts: [],
dds: [],
},
subClass:
'w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6',
},
{
title: 'total',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
key: 'total',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
},
{
title: 'hit',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'hit',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'hit_ratio',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'hit_ratio',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'size',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'size',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'size_avg',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'size_avg',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'ip',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'uniq_ip',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: 'referrer',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'uniq_referrer',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: '갱신일',
class: 'px-3 py-3.5 text-left text-sm font-semibold text-gray-900',
key: 'updated',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'px-3 py-4 text-sm text-gray-500',
},
];
const listActions = [];
const actionKey = 'serial';
const listKeys = [
'serial',
'date_tag',
'total',
'hit',
'size',
'uniq_ip',
'uniq_referrer',
'updated',
];
const listData = ref([]);
const totalPageCount = ref(0);
const currentPageNumber = ref(1);
const pageSize = ref(26);
const recordsTotal = ref(0);
// const order = [{ column: 'serial', dir: 'desc' }];
// const columns = { serial: { data: 'serial' } };
function columnFilter(key, val) {
// console.log('columnFilter(), key = ', key, ', val = ', val);
if (key == 'updated' || key == 'created') {
return $dayjs(val).format('YY/MM/DD A h:mm:ss');
// return $dayjs(val).format('YY/MM/DD');
} else if (key == 'size') {
return _utils.formatBytes(val, 2);
} else {
return val;
}
}
function doAction(tag, target) {
console.log('on doAction(), tag=', tag, ', target=', target);
router.push({
name: 'key-edit',
params: { target: target },
});
}
function pageMove(targetPageIdex) {
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function refresh() {
const responseJson = await _crossCtl.doComm(
'local/list',
'admin:statistics',
{
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
hero: targetKey.value,
term: targetTerm.value,
// termPrefix: _utils.getDateTimeTag(targetTerm.value.substring(0, 1)),
// termPrefix: _utils.getDateTimeTag('y'),
termPrefix: targetDate.value,
}
);
currentPageNumber.value = responseJson['currentPageNumber'];
totalPageCount.value = responseJson['totalPageCount'];
pageSize.value = parseInt(responseJson['pageSize']);
recordsTotal.value = responseJson['recordsTotal'];
for (let i = 0; i < responseJson['data'].length; i++) {
responseJson['data'][i]['hit_ratio'] =
(
(responseJson['data'][i]['hit'] /
responseJson['data'][i]['total']) *
100
).toFixed(2) + '%';
responseJson['data'][i]['size_avg'] = _utils.formatBytes(
responseJson['data'][i]['size'] / responseJson['data'][i]['total'],
2
);
}
listData.value = responseJson['data'];
console.log('listData.value=', listData.value);
}
const terms = ref([]);
const route = useRoute();
const hero = route.params.hero;
const targetTerm = ref('year');
const targetKey = ref('');
const tmpTerms = [
{ current: targetTerm.value == 'year', key: 'year' },
{ current: targetTerm.value == 'month', key: 'month' },
{ current: targetTerm.value == 'day', key: 'day' },
{ current: targetTerm.value == 'hour', key: 'hour' },
];
terms.value = tmpTerms;
const responseJson = await _crossCtl.doComm('local/select', 'admin:key', {
hero: hero,
});
if (responseJson['data'].length == 0) {
_crossCtl.openModal(
'error',
'잘못된 파라메타',
'키 정보를 읽어올 수 없습니다.\n확인 버튼을 누르면 메인 화면으로 돌아갑니다.',
['확인'],
(btnIdx) => {
navigateTo('/');
}
);
} else {
targetKey.value = responseJson['data'][0]['api_key'];
}
function handleDate(date) {
console.log('huk date = ', date);
let result = date;
switch (targetTerm.value) {
case 'year':
result = `${date}`;
break;
case 'month':
result = `${date}`;
break;
case 'day':
result = `${date.year}${
(date.month + 1 < 10 ? '0' : '') + (date.month + 1)
}`;
break;
case 'hour':
result = $dayjs(date).format('YYYYMMDD');
break;
}
targetDate.value = result;
console.log('huk result = ', result);
refresh();
}
function onChange(e) {
console.log('targetTerm.value=', targetTerm.value);
switch (targetTerm.value) {
case 'year':
targetDate.value = $dayjs(new Date().toISOString()).format('YYYY');
break;
case 'month':
targetDate.value = $dayjs(new Date().toISOString()).format('YYYY');
break;
case 'day':
targetDate.value = $dayjs(new Date().toISOString()).format(
'YYYYMM'
);
break;
case 'hour':
targetDate.value = $dayjs(new Date().toISOString()).format(
'YYYYMMDD'
);
break;
}
terms.value = [
{ current: targetTerm.value == 'year', key: 'year' },
{ current: targetTerm.value == 'month', key: 'month' },
{ current: targetTerm.value == 'day', key: 'day' },
{ current: targetTerm.value == 'hour', key: 'hour' },
];
refresh();
}
refresh();
</script>

View File

@@ -0,0 +1,36 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="bg-white">
<div class="max-w-7xl mx-auto py-16 px-4 sm:py-24 sm:px-6 lg:px-8">
<div class="text-center">
<h2
class="text-base font-semibold text-indigo-600 tracking-wide uppercase"
>
어드민 / API
</h2>
<p
class="mt-1 text-4xl font-extrabold text-gray-900 sm:text-5xl sm:tracking-tight lg:text-6xl"
>
특정 키의 사용량 정보 보기
</p>
<p class="max-w-xl mt-5 mx-auto text-xl text-gray-500">
현재 선택된 키의 상세 사용량 정보를 있습니다.
</p>
<br />
있는 페이지 :
<a href="javascript:void(0)" @click="$router.back()">
이전 페이지
</a>
,
<a href="javascript:void(0)" @click="$router.push('/')"> </a>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'check-auth-admin',
});
</script>

View File

@@ -0,0 +1,180 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<br />
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
삭제된 리스트
</h1>
<p class="mt-2 text-sm text-gray-700">
삭제된 API 키를 보고 복구할 있습니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button
type="button"
class="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"
@click="$router.push('/admin/key/list')"
>
전체 리스트
</button>
</div>
</div>
<BaseList1
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="listData"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<BasePagination1
:total-page-count="totalPageCount"
:current-page-number="currentPageNumber"
:page-size="pageSize"
:records-total="recordsTotal"
:page-move="pageMove"
/>
<div class="p-0 rounded-bl-2xl rounded-br-2xl md:px-0"></div>
</div>
</template>
<script setup lang="ts">
const router = useRouter();
definePageMeta({
middleware: 'check-auth-admin',
});
const route = useRoute();
const targetUID = ref(route.query.uid ? route.query.uid : 'all');
const listHeadings = [
{
title: '이름',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'name',
hiddenInfo: {
headClass: 'font-normal lg:hidden',
dts: [
{ class: 'sr-only', title: '단계' },
{ class: 'sr-only sm:hidden', title: '상태' },
],
dds: [
{ class: 'mt-1 truncate text-gray-700', key: 'level' },
{
class: 'mt-1 truncate text-gray-500 sm:hidden',
key: 'status',
},
],
},
subClass:
'w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6',
},
{
title: '단계',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
key: 'level',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
},
{
title: '상태',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'status',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: '생성일',
class: 'px-3 py-3.5 text-left text-sm font-semibold text-gray-900',
key: 'created',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'px-3 py-4 text-sm text-gray-500',
},
];
const listActions = ['상세보기'];
const actionKey = 'serial';
const listKeys = ['serial', 'api_key', 'name', 'level', 'status', 'created'];
const listData = ref([]);
const totalPageCount = ref(0);
const currentPageNumber = ref(1);
const pageSize = ref(10);
const recordsTotal = ref(0);
// const order = [{ column: 'serial', dir: 'desc' }];
// const columns = { serial: { data: 'serial' } };
const { $dayjs } = useNuxtApp();
function columnFilter(key, val) {
// console.log('columnFilter(), key = ', key, ', val = ', val);
if (key == 'updated' || key == 'created') {
// return $dayjs(val).format('YY/MM/DD A h:mm:ss');
return $dayjs(val).format('YY/MM/DD');
} else {
return val;
}
}
function maekNewKey() {
return navigateTo({
path: '/key/new',
query: {},
});
}
function doAction(tag, target) {
console.log('on doAction(), tag=', tag, ', target=', target);
/*
router.push({
name: 'key-edit',
params: { target: target },
});
*/
navigateTo('/admin/key/' + target + '/edit');
}
function pageMove(targetPageIdex) {
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function refresh() {
if (process.client) {
const responseJson = await _crossCtl.doComm(
'local/list',
'admin:key:deleted',
{
hero: targetUID.value,
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
}
);
currentPageNumber.value = responseJson['currentPageNumber'];
totalPageCount.value = responseJson['totalPageCount'];
pageSize.value = parseInt(responseJson['pageSize']);
recordsTotal.value = responseJson['recordsTotal'];
listData.value = responseJson['data'];
}
}
refresh();
</script>

View File

@@ -0,0 +1,261 @@
<template>
<div class="m-8">
<form
class="space-y-8 divide-y divide-gray-200"
@submit.prevent="doUpdate"
>
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
<div
class="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5"
>
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
API 상세 보기
</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">
API키의 설정을 변경하거나 삭제할 있습니다.
</p>
</div>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5"
>
<label
for="name"
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
이름
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<input
id="name"
v-model="name"
type="text"
name="name"
autocomplete="name"
class="max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</div>
</div>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5"
>
<label
for="api-key"
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
API
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<input
id="api-key"
v-model="apiKey"
type="text"
name="api-key"
autocomplete="api-key"
disabled
class="max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</div>
</div>
<div
class="space-y-6 sm:space-y-5 divide-y divide-gray-200"
>
<div class="pt-6 sm:pt-5">
<div
role="group"
aria-labelledby="label-notifications"
>
<div
class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline"
>
<div>
<div
id="label-notifications"
class="text-base font-medium text-gray-900 sm:text-sm sm:text-gray-700"
>
필터 단계
</div>
</div>
<div class="sm:col-span-2">
<div class="max-w-lg">
<p class="text-sm text-gray-500">
필터 대상이 되는 단계를
선택합니다. (현재는 최대 필터만
동작)
</p>
<div class="mt-4 space-y-4">
<div class="flex items-center">
<input
id="filter-high"
v-model="level"
value="high"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-high"
class="ml-3 block text-sm font-medium text-gray-700"
>
최대 필터
</label>
</div>
<div class="flex items-center">
<input
id="filter-mid"
v-model="level"
value="mid"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-mid"
class="ml-3 block text-sm font-medium text-gray-700"
>
중간 필터
</label>
</div>
<div class="flex items-center">
<input
id="filter-low"
v-model="level"
value="low"
name="filter-level"
type="radio"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-low"
class="ml-3 block text-sm font-medium text-gray-700"
>
가장 가벼운 필터
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="pt-5">
<div class="flex justify-between">
<div>
<button
type="button"
class="bg-red-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
@click="doDelete"
>
{{ status == 0 ? '삭제' : '복구' }}
</button>
</div>
<div>
<button
type="button"
class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="doCancel"
>
이전화면
</button>
<button
v-if="status == 0"
type="submit"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
저장
</button>
</div>
</div>
</div>
</form>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const router = useRouter();
definePageMeta({
middleware: 'check-auth-admin',
});
const hero = route.params.target;
console.log('hero=', hero);
const apiKey = ref('');
const name = ref('');
const level = ref('');
const status = ref(0);
const responseJson = await _crossCtl.doComm('local/select', 'admin:key', {
hero: hero,
});
const keyInfo = responseJson['data'][0];
apiKey.value = keyInfo['api_key'];
name.value = keyInfo['name'];
level.value = keyInfo['level'];
status.value = keyInfo['status'];
async function doUpdate() {
const responseJson = await _crossCtl.doComm('local/update', 'admin:key', {
hero: hero,
name: name.value,
level: level.value,
status: status.value,
});
console.log('responseJson=', responseJson);
if (responseJson['responseMessage'] == 'ok') {
alert('ok');
} else {
alert(responseJson['responseMessage']);
}
}
async function doDelete() {
if (status.value == 0) {
const responseJson = await _crossCtl.doComm(
'local/delete',
'admin:key',
{
hero: hero,
}
);
console.log('responseJson=', responseJson);
if (responseJson['responseMessage'] == 'ok') {
status.value = 4;
alert('ok');
} else {
alert(responseJson['responseMessage']);
}
} else {
const responseJson = await _crossCtl.doComm(
'local/update',
'admin:key',
{
hero: hero,
name: name.value,
level: level.value,
status: 0,
}
);
console.log('responseJson=', responseJson);
if (responseJson['responseMessage'] == 'ok') {
status.value = 0;
alert('ok');
} else {
alert(responseJson['responseMessage']);
}
}
}
async function doCancel() {
router.back();
}
</script>

View File

@@ -0,0 +1,192 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<br />
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900"> 리스트</h1>
<p class="mt-2 text-sm text-gray-700">
전체 사용자의 API 키를 보고 관리할 있습니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button
type="button"
class="inline-flex mr-3 items-center justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 sm:w-auto"
@click="$router.push('/admin/key/deleted')"
>
삭제 리스트
</button>
</div>
</div>
<BaseList1
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="listData"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<BasePagination1
:total-page-count="totalPageCount"
:current-page-number="currentPageNumber"
:page-size="pageSize"
:records-total="recordsTotal"
:page-move="pageMove"
/>
<div class="p-0 rounded-bl-2xl rounded-br-2xl md:px-0"></div>
</div>
</template>
<script setup lang="ts">
const router = useRouter();
definePageMeta({
middleware: 'check-auth-admin',
});
const route = useRoute();
const targetUID = ref(route.query.uid ? route.query.uid : 'all');
const listHeadings = [
{
title: '소유자',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'by',
hiddenInfo: {
headClass: 'font-normal lg:hidden',
dts: [],
dds: [],
},
subClass:
'w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6',
},
{
title: '이름',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'name',
hiddenInfo: {
headClass: 'font-normal lg:hidden',
dts: [
{ class: 'sr-only', title: '단계' },
{ class: 'sr-only sm:hidden', title: '상태' },
],
dds: [
{ class: 'mt-1 truncate text-gray-700', key: 'level' },
{
class: 'mt-1 truncate text-gray-500 sm:hidden',
key: 'status',
},
],
},
subClass:
'w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6',
},
{
title: '단계',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
key: 'level',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
},
{
title: '상태',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'status',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: '생성일',
class: 'px-3 py-3.5 text-left text-sm font-semibold text-gray-900',
key: 'created',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'px-3 py-4 text-sm text-gray-500',
},
];
const listActions = ['상세보기'];
const actionKey = 'serial';
const listKeys = [
'serial',
'by',
'api_key',
'name',
'level',
'status',
'created',
];
const listData = ref([]);
const totalPageCount = ref(0);
const currentPageNumber = ref(1);
const pageSize = ref(10);
const recordsTotal = ref(0);
// const order = [{ column: 'serial', dir: 'desc' }];
// const columns = { serial: { data: 'serial' } };
const { $dayjs } = useNuxtApp();
function columnFilter(key, val) {
// console.log('columnFilter(), key = ', key, ', val = ', val);
if (key == 'updated' || key == 'created') {
// return $dayjs(val).format('YY/MM/DD A h:mm:ss');
return $dayjs(val).format('YY/MM/DD');
} else {
return val;
}
}
function doAction(tag, target) {
console.log('on doAction(), tag=', tag, ', target=', target);
/*
router.push({
name: 'admin-key-edit',
params: { target: target },
});
*/
navigateTo('/admin/key/' + target + '/edit');
}
function pageMove(targetPageIdex) {
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function refresh() {
if (process.client) {
const responseJson = await _crossCtl.doComm(
'local/list',
'admin:key:active',
{
hero: targetUID.value,
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
}
);
currentPageNumber.value = responseJson['currentPageNumber'];
totalPageCount.value = responseJson['totalPageCount'];
pageSize.value = parseInt(responseJson['pageSize']);
recordsTotal.value = responseJson['recordsTotal'];
listData.value = responseJson['data'];
}
}
refresh();
</script>

View File

@@ -0,0 +1,487 @@
<template>
<div>
<div class="space-y-6 lg:col-start-1 lg:col-span-2">
<!-- Description list-->
<section aria-labelledby="applicant-indivation-title">
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h2
id="applicant-indivation-title"
class="text-lg leading-6 font-medium text-gray-900"
>
게시판에 글을 작성하는 화면에서 API 사용
</h2>
<p class="mt-1 max-w-2xl text-sm text-gray-500">
문제가 있는 표현을 고지하고 수정하기 전에는 저장을
하지 못하게 합니다.
</p>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<div>
<div
class="shadow sm:rounded-md sm:overflow-hidden"
>
<div
class="px-4 py-5 bg-white space-y-6 sm:p-6"
>
<div class="grid grid-cols-3 gap-6">
<div class="col-span-3 sm:col-span-3">
<label
for="company-website"
class="block text-sm font-medium text-gray-700"
>
제목
</label>
<div
class="mt-1 flex rounded-md shadow-sm"
>
<input
id="company-website"
v-model="boardTitle"
type="text"
name="company-website"
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300"
placeholder=""
/>
</div>
</div>
</div>
<div>
<label
for="about"
class="block text-sm font-medium text-gray-700"
>
내용
</label>
<div class="mt-1">
<textarea
id="about"
v-model="boardText"
name="about"
rows="7"
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md"
placeholder=""
/>
</div>
<p class="mt-2 text-sm text-gray-500">
모욕적인 표현이 포함된 경우 저장을
하실 없습니다.
</p>
</div>
</div>
<div
class="px-4 py-3 bg-gray-50 text-right sm:px-6"
>
<SwitchGroup
as="div"
class="flex items-center"
>
<Switch
v-model="boardErrorEnabled"
:class="[
boardErrorEnabled
? 'bg-indigo-600'
: 'bg-gray-200',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500',
]"
>
<span
aria-hidden="true"
:class="[
boardErrorEnabled
? 'translate-x-5'
: 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
]"
/>
</Switch>
<SwitchLabel as="span" class="ml-3">
<span
class="text-sm font-medium text-gray-900"
>API 오류 시뮬레이션
</span>
</SwitchLabel>
</SwitchGroup>
<button
type="button"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="handleBoard"
>
전송
</button>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Comments-->
<section aria-labelledby="notes-title">
<div class="bg-white 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"
>
댓글 작성 화면에서의 API 사용
</h2>
</div>
<div class="px-4 py-6 sm:px-6">
<ul role="list" class="space-y-8">
<li
v-for="comment in comments"
:key="comment.id"
>
<div class="flex space-x-3">
<div class="flex-shrink-0">
<img
class="h-10 w-10 rounded-full"
:src="`https://images.unsplash.com/photo-${comment.imageId}?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=divat&fit=facearea&facepad=2&w=256&h=256&q=80`"
alt=""
/>
</div>
<div>
<div class="text-sm">
<a
href="#"
class="font-medium text-gray-900"
>{{ comment.name }}</a
>
</div>
<div
class="mt-1 text-sm text-gray-700"
>
<p>
{{ comment.body }}
</p>
</div>
<div class="mt-2 text-sm space-x-2">
<span
class="text-gray-500 font-medium"
>{{ comment.date }}</span
>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
<div class="bg-gray-50 px-4 py-6 sm:px-6">
<div class="flex space-x-3">
<div class="flex-shrink-0">
<img
class="h-10 w-10 rounded-full"
:src="user.imageUrl"
alt=""
/>
</div>
<div class="min-w-0 flex-1">
<div action="#">
<div>
<label for="comment" class="sr-only"
>About</label
>
<textarea
id="comment"
v-model="commentMessage"
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="javascript:void(0)"
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>
<SwitchGroup
as="div"
class="flex items-center"
>
<Switch
v-model="commentErrorEnabled"
:class="[
commentErrorEnabled
? 'bg-indigo-600'
: 'bg-gray-200',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500',
]"
>
<span
aria-hidden="true"
:class="[
commentErrorEnabled
? 'translate-x-5'
: 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
]"
/>
</Switch>
<SwitchLabel as="span" class="ml-3">
<span
class="text-sm font-medium text-gray-900"
>API 오류 시뮬레이션
</span>
</SwitchLabel>
</SwitchGroup>
<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="handleComment"
>
전송
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- Global notification live region, render this permanently at the end of the document -->
<div
aria-live="assertive"
class="fixed inset-0 flex items-end px-4 py-6 pointer-events-none sm:p-6 sm:items-start"
>
<div
class="w-full flex flex-col items-center space-y-4 sm:items-end"
>
<!-- Notification panel, dynamically insert this into the live region when it needs to be displayed -->
<transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="notiShow"
class="max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden"
>
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<CheckCircleIcon
v-if="isGoodFlag"
class="h-6 w-6 text-green-400"
aria-hidden="true"
/>
<ExclamationCircleIcon
v-else
class="h-6 w-6 text-red-400"
aria-hidden="true"
/>
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p
class="text-sm font-medium text-gray-900"
>
{{ notiTitle }}
</p>
<p class="mt-1 text-sm text-gray-500">
{{ notiMessage }}
</p>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button
type="button"
class="bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="notiShow = false"
>
<span class="sr-only">Close</span>
<XIcon
class="h-5 w-5"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
</div>
</transition>
</div>
</div>
</div>
</template>
<script setup>
import { XMarkIcon, QuestionMarkCircleIcon } from '@heroicons/vue/24/solid';
import {
CheckCircleIcon,
ExclamationCircleIcon,
} from '@heroicons/vue/24/outline';
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
const user = {
name: 'Whitney Francis',
email: 'whitney@example.com',
imageUrl:
'https://images.unsplash.com/photo-1517365830460-955ce3ccd263?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=divat&fit=facearea&facepad=8&w=256&h=256&q=80',
};
let commentSerial = 0;
const boardErrorEnabled = ref(false);
const commentErrorEnabled = ref(false);
const comments = ref([
{
id: ++commentSerial,
name: '김재순',
date: '하루 전',
imageId: '1506794778202-cad84cf45f1d',
body: '짜장면이 맛이 있느냐 짬뽕이 맛이 있느냐 논쟁 같은 느낌인데요? ',
},
]);
const notiShow = ref(false);
const isGoodFlag = ref(false);
const notiTitle = ref('');
const notiMessage = ref('');
const notiTimer = null;
function showNotifications(isGood, title, message) {
isGoodFlag.value = isGood;
notiTitle.value = title;
notiMessage.value = message;
notiShow.value = true;
if (notiTimer != null) {
clearTimeout(notiTimer);
notiTimer = null;
}
notiTimer = setTimeout(() => {
notiShow.value = false;
}, 8000);
}
const boardTitle = ref('');
const boardText = ref('');
const commentMessage = ref('');
const missingParts = 'e3';
async function handleFilter(source, at, errorSimulFlag, cb) {
const responseJson = await _crossCtl.doFilter(
'' + (errorSimulFlag == false ? '' : missingParts),
{
text: source,
mode: 'quick',
}
);
// console.log('responseJson=', responseJson);
if (responseJson['Status']['Code'] == 2000) {
// Detected
if (responseJson['Detected'].length == 0) {
cb(null);
} else {
showNotifications(
false,
'잘못된 표현',
at +
'에 이 표현은 쓸 수 없습니다. : ' +
responseJson['Detected'][0][1]
);
cb(false);
}
} else {
showNotifications(
false,
'오류',
'API 호출 오류 : ' + responseJson['Status']['Message']
);
cb(true);
}
}
function handleBoard() {
// console.log(boardTitle.value);
// console.log(boardText.value);
if (boardTitle.value.trim() == '') {
showNotifications(false, '오류', '제목을 입력해 주세요.');
return;
}
if (boardText.value.trim() == '') {
showNotifications(false, '오류', '본문을 입력해 주세요.');
return;
}
handleFilter(
boardTitle.value,
'게시판 제목',
boardErrorEnabled.value,
function (error) {
if (error == null) {
handleFilter(
boardText.value,
'게시판 본문',
boardErrorEnabled.value,
function (error) {
if (error == null) {
showNotifications(
true,
'게시 가능',
'입력하신 제목과 내용에 문제가 없습니다.'
);
}
}
);
}
}
);
}
function handleComment() {
if (commentMessage.value == '') {
return;
}
handleFilter(
commentMessage.value,
'댓글',
commentErrorEnabled.value,
function (error) {
console.log('error=', error);
if (error == null) {
comments.value.push({
id: ++commentSerial,
name: '당신',
date: '조금 전',
imageId: '1517365830460-955ce3ccd263',
body: commentMessage.value,
});
commentMessage.value = '';
} else if (error == true) {
} else {
}
}
);
}
</script>

View File

@@ -0,0 +1,77 @@
<!--
This example requires Tailwind CSS v2.0+
This example requires some changes to your config:
```
// tailwind.config.js
module.exports = {
// ...
plugins: [
// ...
require('@tailwindcss/forms'),
],
}
```
-->
<template>
<div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form action="#" method="POST">
<div class="shadow sm:rounded-md sm:overflow-hidden">
<div class="px-4 py-5 bg-white space-y-6 sm:p-6">
<div class="grid grid-cols-3 gap-6">
<div class="col-span-3 sm:col-span-3">
<label
for="company-website"
class="block text-sm font-medium text-gray-700"
>
제목
</label>
<div class="mt-1 flex rounded-md shadow-sm">
<input
id="company-website"
type="text"
name="company-website"
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300"
placeholder=""
/>
</div>
</div>
</div>
<div>
<label
for="about"
class="block text-sm font-medium text-gray-700"
>
내용
</label>
<div class="mt-1">
<textarea
id="about"
name="about"
rows="7"
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md"
placeholder=""
/>
</div>
<p class="mt-2 text-sm text-gray-500">
모욕적인 표현이 포함된 경우 저장을 하실
없습니다.
</p>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button
type="submit"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save
</button>
</div>
</div>
</form>
</div>
</div>
</template>

View File

@@ -0,0 +1,187 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="">
<div class="container mx-auto">
<div class="min-w-full border rounded lg:grid lg:grid-cols-3">
<div class="hidden lg:col-span-3 lg:block">
<div class="flex flex-col min-h-screen w-full">
<div
class="relative flex items-center p-3 border-b border-gray-300"
>
<img
class="object-cover w-10 h-10 rounded-full"
src="https://cdn.pixabay.com/photo/2018/01/15/07/51/woman-3083383__340.jpg"
alt="username"
/>
<span class="block ml-2 font-bold text-gray-600">
채팅 상황에서의 API 사용 {{ ' ' }}
<p class="text-xs">
{{
messageList[messageList.length - 1][
'elapsedServer'
] != ''
? messageList[
messageList.length - 1
]['elapsedServer'] +
' / ' +
messageList[
messageList.length - 1
]['elapsed']
: ''
}}
</p>
</span>
<span
class="absolute w-3 h-3 bg-green-600 rounded-full left-10 top-3"
>
</span>
</div>
<div class="flex-grow bg-gray-50 justify-center">
<div
class="relative w-full p-6 overflow-y-auto max-h-fit"
>
<ul class="space-y-2">
<li
v-for="message in messageList"
:key="message['serial']"
class="flex"
:class="
message['left'] == true
? 'justify-start'
: 'justify-end'
"
>
<div
class="relative max-w-xl px-4 py-2 text-gray-700 rounded shadow"
>
<span class="block">{{
message['text']
}}</span>
</div>
</li>
</ul>
</div>
</div>
<div
class="flex items-center justify-between w-full p-3 border-t border-gray-300"
>
<input
v-model="justEnteredText"
type="text"
placeholder="Message"
class="block w-full py-2 pl-4 mx-3 bg-gray-100 rounded-full outline-none focus:text-gray-700"
name="message"
required
@keyup.enter="checkChatInput"
/>
<button type="button" @click="checkChatInput">
<svg
class="w-5 h-5 text-gray-500 origin-center transform rotate-90"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"
/>
</svg>
</button>
</div>
<div
class="flex items-center justify-end w-full p-3 border-t border-gray-300"
></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'check-auth-user',
});
let tmpIdx = 0;
const messageList = ref([
{
serial: tmpIdx++,
left: true,
text: '안녕하세요?',
elapsedServer: '',
elapsed: '',
},
{
serial: tmpIdx++,
left: false,
text: '테스트 하러 들어 왔습니다.',
elapsedServer: '',
elapsed: '',
},
{
serial: tmpIdx++,
left: true,
text: '테스트 하면 이 문장이죠.',
elapsedServer: '',
elapsed: '',
},
{
serial: tmpIdx++,
left: true,
text: 'Lorem ipsum dolor sit, amet consectetur adipisicing elit.',
elapsedServer: '',
elapsed: '',
},
]);
const justEnteredText = ref('');
const inPregressFlag = ref(false);
const elispe = ref(0);
async function checkChatInput() {
console.log('huk');
if (justEnteredText.value == '') {
return;
}
if (inPregressFlag.value == true) {
return;
}
inPregressFlag.value = true;
const startTime = performance.now();
const responseJson = await _crossCtl.doFilter('', {
text: justEnteredText.value,
mode: 'filter',
});
const endTime = performance.now();
inPregressFlag.value = false;
elispe.value = endTime - startTime;
console.log('responseJson=', responseJson);
if (responseJson['Status']['Code'] == 2000) {
messageList.value.push({
serial: tmpIdx++,
left: false,
text: responseJson['Filtered'],
elapsedServer: responseJson['Elapsed'].replace('0 s, ', ''),
elapsed: elispe.value.toFixed(2) + 'ms',
});
justEnteredText.value = '';
} else {
alert(responseJson['Status']['Message']);
}
}
</script>

View File

@@ -0,0 +1,45 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
API 테스트 기능
</h1>
<p class="mt-2 text-sm text-gray-700">
API 키가 실제 서비스 예에서 사용되는 것을 있습니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"></div>
</div>
<nav class="mt-5 space-y-1" aria-label="Sidebar">
<a
v-for="item in navigation"
:key="item.name"
href="javascript:void(0)"
:class="[
'text-gray-600 hover:bg-gray-100 hover:text-gray-900',
'flex items-center px-3 py-2 text-sm font-medium rounded-md',
]"
:aria-current="'page'"
@click="$router.push(item.href)"
>
<span class="truncate">
{{ item.name }}
</span>
</a>
</nav>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'check-auth-op',
});
const navigation = [
{ name: 'API 키 테스트 게시판', href: '/admin/lab/board' },
{ name: 'API 키 테스트 웹 채팅', href: '/admin/lab/chatting' },
];
</script>

View File

@@ -0,0 +1,79 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
통계 테스트 페이지
</h1>
<p class="mt-2 text-sm text-gray-700">
통계 개발과 기능 테스트를 위해 만들어진 임시 페이지입니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"></div>
</div>
<nav class="mt-5 space-y-1" aria-label="Sidebar">
<a
v-for="item in navigation"
:key="item.name"
href="javascript:void(0)"
:class="[
'text-gray-600 hover:bg-gray-100 hover:text-gray-900',
'flex items-center px-3 py-2 text-sm font-medium rounded-md',
]"
:aria-current="'page'"
@click="doAction(item.actionTag)"
>
<span class="truncate">
{{ item.name }}
</span>
</a>
</nav>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'check-auth-op',
});
const navigation = [
{ name: 'hello?', actionTag: 'hello' },
{ name: '2022년 통계 가공', actionTag: '2022' },
{ name: '2022년 9월 통계 가공', actionTag: '202209' },
{ name: '2022년 9월 27일 통계 가공', actionTag: '20220927' },
];
async function doAction(tag) {
let responseJson = null;
switch (tag) {
case '2022':
responseJson = await _crossCtl.doComm('local/lab', 'makestat', {
termTag: 'year',
dateTag: '2022',
});
console.log('responseJson=', responseJson);
break;
case '202209':
responseJson = await _crossCtl.doComm('local/lab', 'makestat', {
termTag: 'month',
dateTag: '202209',
});
console.log('responseJson=', responseJson);
break;
case '20220927':
responseJson = await _crossCtl.doComm('local/lab', 'makestat', {
termTag: 'day',
dateTag: '20220927',
});
console.log('responseJson=', responseJson);
break;
case 'hello':
responseJson = await _crossCtl.doComm('local/lab', 'hello', {});
console.log('responseJson=', responseJson);
break;
}
}
</script>

View File

@@ -0,0 +1,328 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
API 사용 통계 {{ hero == 'usage' ? '(사용량)' : '(단어)' }}
</h1>
<p class="mt-2 text-sm text-gray-700">
사용량 통계를 보여 줍니다. 구분 항목으로 시간별, 날짜별,
월별 구분이 가능합니다.
</p>
</div>
<select
id="targetUnit"
v-model="targetUnit"
name="targetUnit"
class="mt-0 block pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
@change="onChange($event)"
>
<option
v-for="unit in units"
:key="unit.key"
:selected="unit.current"
:value="unit.key"
>
<span class="truncate">
{{ unit.key }}
</span>
</option>
</select>
<Datepicker
v-model="date"
class="w-64 mt-4 sm:mt-0 sm:ml-2 sm:flex-none"
range
multi-calendars
multi-calendars-solo
:format="inputFormat"
:preview-format="previewFormat"
@update:modelValue="handleDate"
/>
</div>
<StatisticsTable1
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="listData"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<div class="p-0 rounded-bl-2xl rounded-br-2xl md:px-0">
<button
type="button"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none"
@click="doDownload()"
>
<span> 엑셀파일로 다운로드 </span>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import Datepicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
definePageMeta({
middleware: 'check-auth-admin',
});
const router = useRouter();
const route = useRoute();
const hero = ref(route.params.hero);
const targetUID = ref(route.query.uid ? route.query.uid : 'all');
const targetKey = ref(route.query.key ? route.query.key : 'all');
const targetUnit = ref(route.query.unit ? route.query.unit : 'day');
const { $dayjs } = useNuxtApp();
const targetDate = ref($dayjs(new Date().toISOString()).format('YYYY'));
const targetDateMonth = ref($dayjs(new Date().toISOString()).format('YYYY'));
const targetDateDay = ref({
month: new Date().getMonth(),
year: new Date().getFullYear,
});
const targetDateHour = ref($dayjs(new Date().toISOString()));
const units = ref([
{ current: targetUnit.value == 'year', key: 'year' },
{ current: targetUnit.value == 'month', key: 'month' },
{ current: targetUnit.value == 'day', key: 'day' },
{ current: targetUnit.value == 'hour', key: 'hour' },
]);
function onChange(e) {
console.log('targetUnit.value=', targetUnit.value);
navigateTo(
'/admin/statistics/byterm/' +
hero.value +
'?unit=' +
targetUnit.value +
'&uid=' +
targetUID.value +
'&key=' +
targetKey.value
);
refresh();
units.value = [
{ current: targetUnit.value == 'year', key: 'year' },
{ current: targetUnit.value == 'month', key: 'month' },
{ current: targetUnit.value == 'day', key: 'day' },
{ current: targetUnit.value == 'hour', key: 'hour' },
];
refresh();
}
const date = ref([]);
const inputFormat = (date) => {
// console.log('huk date=', date);
if (date.length == 1) {
const day = date[0].getDate();
const month = date[0].getMonth() + 1;
const year = date[0].getFullYear();
return `${day}/${month}/${year}`;
} else if (date.length == 2) {
const day1 = date[0].getDate();
const month1 = date[0].getMonth() + 1;
const year1 = date[0].getFullYear();
const day2 = date[1].getDate();
const month2 = date[1].getMonth() + 1;
const year2 = date[1].getFullYear();
return `${year1}-${month1}-${day1} ~ ${year2}-${month2}-${day2}`;
}
};
const previewFormat = (date) => {
// console.log('huk date=', date);
if (date.length == 1) {
const day = date[0].getDate();
const month = date[0].getMonth() + 1;
const year = date[0].getFullYear();
return `${day}/${month}/${year}`;
} else if (date.length == 2) {
const day1 = date[0].getDate();
const month1 = date[0].getMonth() + 1;
const year1 = date[0].getFullYear();
const day2 = date[1].getDate();
const month2 = date[1].getMonth() + 1;
const year2 = date[1].getFullYear();
return `${year1}-${month1}-${day1} ~ ${year2}-${month2}-${day2}`;
}
};
const endDate = new Date();
const startDate = new Date(new Date().setDate(endDate.getDate() - 7));
date.value = [startDate, endDate];
const startDateTag = ref($dayjs(startDate.toISOString()).format('YYYYMMDDHH'));
const endDateTag = ref($dayjs(endDate.toISOString()).format('YYYYMMDDHH'));
function doDownload() {
const anchor = document.createElement('a');
let urlBase = '';
const currentHost = window.location.host.toLowerCase();
const currentProtocol = window.location.protocol;
const currentDomain = _utils.getDomain(window.location.href);
const apiBaseUrl = _crossCtl.config['API_BASE_URL'];
console.log('currentHost=', currentHost);
console.log('currentProtocol=', currentProtocol);
console.log('currentDomain=', currentDomain);
console.log('apiBaseUrl=', apiBaseUrl);
if (apiBaseUrl.indexOf(currentHost) == -1) {
urlBase = apiBaseUrl;
} else {
urlBase = '/api/';
}
console.log('urlBase=', urlBase);
anchor.href =
urlBase +
'local/download/report_' +
hero.value +
'_' +
startDateTag.value +
'_' +
endDateTag.value +
'.xlsx?tag=' +
hero.value +
'&startDateTag=' +
startDateTag.value +
'&endDateTag=' +
endDateTag.value +
'&unit=' +
targetUnit.value +
'&uid=' +
targetUID.value +
'&key=' +
targetKey.value;
anchor.target = '_blank';
anchor.click();
}
const listHeadings =
hero.value == 'usage'
? [
{
title: 'date',
key: 'date_tag',
},
{
title: 'total',
key: 'total',
},
{
title: 'hit',
key: 'hit',
},
{
title: 'size',
key: 'size',
},
]
: [
{
title: 'word',
key: 'word',
widthRatio: '100',
},
{
title: 'count',
key: 'count_sum',
},
];
const listActions = [];
const actionKey = 'serial';
const listKeys = [
'serial',
'date_tag',
'total',
'hit',
'size',
'uniq_ip',
'uniq_referrer',
'updated',
];
const listData = ref([]);
function columnFilter(key, val) {
// console.log('columnFilter(), key = ', key, ', val = ', val);
return val;
}
function doAction(tag, target) {
console.log('on doAction(), tag=', tag, ', target=', target);
router.push({
name: 'key-edit',
params: { target: target },
});
}
async function refresh() {
const responseJson = await _crossCtl.doComm(
'local/select',
hero.value == 'usage'
? 'admin:statistics:usage'
: 'admin:statistics:word',
{
unit: targetUnit.value,
uid: targetUID.value,
key: targetKey.value,
startDateTag: startDateTag.value,
endDateTag: endDateTag.value,
}
);
for (let i = 0; i < responseJson['data'].length; i++) {
responseJson['data'][i]['hit_ratio'] =
(
(responseJson['data'][i]['hit'] /
responseJson['data'][i]['total']) *
100
).toFixed(2) + '%';
responseJson['data'][i]['size_avg'] = _utils.formatBytes(
responseJson['data'][i]['size'] / responseJson['data'][i]['total'],
2
);
}
listData.value = responseJson['data'];
console.log('listData.value=', listData.value);
}
function handleDate(date) {
console.log('huk date = ', date);
startDateTag.value = $dayjs(date[0].toISOString()).format('YYYYMMDDHH');
endDateTag.value = $dayjs(date[1].toISOString()).format('YYYYMMDDHH');
refresh();
}
refresh();
</script>

View File

@@ -0,0 +1,194 @@
<template>
<div class="m-8">
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
<div>
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
전체 사용량 통계
</h1>
<p class="mt-2 text-sm text-gray-700">
전체 서버의 사용량 통계를 확인 합니다.
</p>
</div>
<img
v-if="inPregressFlag"
width="32"
src="/loading-load-2.gif"
/>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button
type="button"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="
navigateTo('/admin/statistics/byterm/usage')
"
>
상세 사용량 통계
</button>
<button
type="button"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="navigateTo('/admin/statistics/byterm/word')"
>
상세 단어 통계
</button>
</div>
</div>
<section aria-labelledby="applicant-information-title">
<div class="bg-white mt-3 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h2
id="applicant-information-title"
class="text-lg font-medium leading-6 text-gray-900"
>
최근 24시간 사용량
</h2>
</div>
<div class="border-t border-gray-200 px-4 py-5 sm:px-6">
<LineChart
:chart-data="lineChartData"
:chart-options="lineChartOptions"
/>
</div>
<!--
<div>
<a
href="javascript:void(0)"
class="block bg-gray-50 px-4 py-4 text-center text-sm font-medium text-gray-500 hover:text-gray-700 sm:rounded-b-lg"
@click="refresh()"
>새로 고침</a
>
</div>
-->
</div>
</section>
<section aria-labelledby="applicant-information-title">
<div class="bg-white mt-3 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h2
id="applicant-information-title"
class="text-lg font-medium leading-6 text-gray-900"
>
최근 24시간 키별 점유율
</h2>
</div>
<div class="border-t border-gray-200 px-4 py-5 sm:px-6">
<PieChart
:chart-data="pieChartData"
:chart-options="pieChartOptions"
/>
</div>
<!--
<div>
<a
href="javascript:void(0)"
class="block bg-gray-50 px-4 py-4 text-center text-sm font-medium text-gray-500 hover:text-gray-700 sm:rounded-b-lg"
@click="refresh()"
>새로 고침</a
>
</div>
-->
</div>
</section>
</div>
</div>
<div class="pt-5">
<div class="flex justify-end"></div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'check-auth-op',
});
const inPregressFlag = ref(true);
const responseJson = await _crossCtl.doComm(
'local/select',
'admin:dashboard',
{}
);
let rawData1 = [];
let rawData2 = [];
console.log('responseJson=', responseJson);
inPregressFlag.value = false;
if (responseJson['responseMessage'] == 'ok') {
rawData1 = responseJson['result']['adminDashData1'];
rawData2 = responseJson['result']['adminDashData2'];
} else {
alert(responseJson['responseMessage']);
}
const lineChartData = {
labels: rawData1.map((item) => item['date_tag']),
datasets: [
{
label: 'Total',
backgroundColor: '#00D8FF',
data: rawData1.map((item) => item['total']),
},
{
label: 'Hit',
backgroundColor: '#f87979',
data: rawData1.map((item) => item['hit']),
},
],
};
const lineChartOptions = {
responsive: true,
maintainAspectRatio: false,
};
const pieChartData = {
labels: rawData2.map((item) => item['key_name']),
datasets: [
{
// backgroundColor: ['#41B883', '#E46651', '#00D8FF', '#DD1B16'],
data: rawData2.map((item) => item['total']),
},
],
};
const pieChartOptions = {
responsive: true,
maintainAspectRatio: false,
};
const chartData = {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
datasets: [
{
label: 'Data One',
backgroundColor: '#f87979',
data: [40, 20, 12, 39, 10, 40, 39, 80, 40, 20, 12, 11],
},
],
};
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
};
</script>

View File

@@ -0,0 +1,121 @@
<template>
<div>
<div class="mt-5 sm:px-3 lg:px-5">전체 통계 페이지 대시보드</div>
<LineChart
:chart-data="lineChartData"
:chart-options="lineChartOptions"
/>
<PieChart :chart-data="pieChartData" :chart-options="pieChartOptions" />
<div class="mt-5 sm:px-3 lg:px-5">
<a
href="javascript:void(0)"
class="text-base font-medium text-indigo-700 hover:text-indigo-600"
@click="navigateTo('/admin/statistics/byterm/usage')"
>기간별 사용량 통계 화면으로 &rarr;</a
>
</div>
<div class="mt-5 sm:px-3 lg:px-5">
<a
href="javascript:void(0)"
class="text-base font-medium text-indigo-700 hover:text-indigo-600"
@click="navigateTo('/admin/statistics/byterm/word')"
>기간별 단어 통계 화면으로 &rarr;</a
>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'check-auth-admin',
});
/*
const route = useRoute();
let hero: string | string[] = '';
hero = route.params['hero'];
*/
const responseJson = await _crossCtl.doComm(
'local/select',
'admin:dashboard',
{}
);
let rawData1 = [];
let rawData2 = [];
console.log('responseJson=', responseJson);
if (responseJson['responseMessage'] == 'ok') {
rawData1 = responseJson['result']['adminDashData1'];
rawData2 = responseJson['result']['adminDashData2'];
}
const lineChartData = {
labels: rawData1.map((item) => item['date_tag']),
datasets: [
{
label: 'Total',
backgroundColor: '#00D8FF',
data: rawData1.map((item) => item['total']),
},
{
label: 'Hit',
backgroundColor: '#f87979',
data: rawData1.map((item) => item['hit']),
},
],
};
const lineChartOptions = {
responsive: true,
maintainAspectRatio: false,
};
const pieChartData = {
labels: rawData2.map((item) => item['key_name']),
datasets: [
{
// backgroundColor: ['#41B883', '#E46651', '#00D8FF', '#DD1B16'],
data: rawData2.map((item) => item['total']),
},
],
};
const pieChartOptions = {
responsive: true,
maintainAspectRatio: false,
};
const chartData = {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
datasets: [
{
label: 'Data One',
backgroundColor: '#f87979',
data: [40, 20, 12, 39, 10, 40, 39, 80, 40, 20, 12, 11],
},
],
};
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
};
</script>

View File

@@ -0,0 +1,272 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<br />
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
필터 단어 로그 보기
</h1>
<p class="mt-2 text-sm text-gray-700">
필터 단어의 생성, 변경, 삭제 기록을 확인할 있습니다.
기록이 없다면 시스템 입력된 단어입니다.
</p>
</div>
<!--
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<label for="mobile-search-candidate" class="sr-only"
>Search</label
>
<label for="desktop-search-candidate" class="sr-only"
>Search</label
>
<div class="flex rounded-md shadow-sm">
<div class="relative flex-grow focus-within:z-10">
<div
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
>
<MagnifyingGlassCircleIcon
class="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</div>
<input
id="mobile-search-candidate"
v-model="searchKeyword"
type="text"
name="mobile-search-candidate"
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:hidden border-gray-300"
placeholder=""
/>
<input
id="desktop-search-candidate"
v-model="searchKeyword"
type="text"
name="desktop-search-candidate"
class="hidden focus:ring-indigo-500 focus:border-indigo-500 w-full rounded-none rounded-l-md pl-10 sm:block sm:text-sm border-gray-300"
placeholder=""
/>
</div>
<button
type="button"
class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-r-md text-gray-700 bg-gray-50 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
@click="doAction('search', searchKeyword)"
>
<span class="ml-2">검색</span>
</button>
</div>
</div>
-->
</div>
<BaseList1
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="listData"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<BasePagination1
:total-page-count="totalPageCount"
:current-page-number="currentPageNumber"
:page-size="pageSize"
:records-total="recordsTotal"
:page-move="pageMove"
/>
<div class="p-0 rounded-bl-2xl rounded-br-2xl md:px-0">
<a
href="javascript:void(0)"
class="text-base font-medium text-indigo-700 hover:text-indigo-600"
@click="$router.back()"
>&larr;이전 화면으로<span aria-hidden="true"> </span
></a>
</div>
</div>
</template>
<script setup lang="ts">
import {
ChevronDownIcon,
MagnifyingGlassCircleIcon,
} from '@heroicons/vue/24/solid';
import consolaGlobalInstance from 'consola';
const { $dayjs } = useNuxtApp();
const router = useRouter();
definePageMeta({
middleware: 'check-auth-admin',
});
const route = useRoute();
console.log('route.params=', route.params);
const targetName = route.params.hero;
const hero = route.params.hero;
console.log('targetName=', targetName);
console.log('hero=', hero);
const listTarget = 'admin:log:word';
const listHeadings = [
{
title: '누가',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'name',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass:
'w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6',
},
{
title: '언제',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
key: 'created',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
},
{
title: '어떻게',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'description',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: '파라메타',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'infos',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
];
const listActions = [];
const actionKey = 'serial';
const listKeys = [
'serial',
'raw',
'level',
'comment',
'status',
'updated',
'created',
];
const listData = ref([]);
const totalPageCount = ref(0);
const currentPageNumber = ref(1);
const pageSize = ref(10);
const recordsTotal = ref(0);
// const order = [{ column: 'serial', dir: 'desc' }];
// const columns = { serial: { data: 'serial' } };
function columnFilter(key, val) {
// console.log("columnFilter(), key = ", key, ", val = ", val);
if (key == 'updated' || key == 'created') {
return $dayjs(val).format('YY/MM/DD A h:mm:ss');
// return $dayjs(val).format('YY/MM/DD');
} else if (key == 'infos') {
const tmpInfosJson = JSON.parse(val);
delete tmpInfosJson.hero;
delete tmpInfosJson.raw;
delete tmpInfosJson.target;
delete tmpInfosJson.status;
delete tmpInfosJson.revive;
const strInfos = JSON.stringify(tmpInfosJson);
return '' + strInfos.substring(1, strInfos.length - 1) + '';
} else if (key == 'status') {
let statusTag = '정상';
switch (val) {
case 0:
statusTag = '정상등록';
break;
case 4:
statusTag = '삭제됨';
break;
default:
statusTag = val;
}
return statusTag;
} else if (key == 'level') {
let levelTag = 'mid';
switch (val) {
case 10:
levelTag = 'high';
break;
case 50:
levelTag = 'mid';
break;
case 100:
levelTag = 'low';
break;
default:
levelTag = val;
}
return levelTag;
} else {
return val;
}
}
function doAction(tag, target) {
console.log('on doAction(), tag=', tag, ', target=', target);
if (tag == '상세보기') {
navigateTo('/admin/user/' + hero + '/history/detail/' + target);
}
}
function pageMove(targetPageIdex) {
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function refresh() {
const responseJson = await _crossCtl.doComm('local/list', listTarget, {
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
hero: hero,
});
console.log('listTarget=', listTarget);
console.log('hero=', hero);
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'];
}
refresh();
</script>

View File

@@ -0,0 +1,366 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="pb-8 px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-xl font-semibold text-gray-900">
필터된 단어 통계 상세 보기
</h1>
<p class="mt-2 text-sm text-gray-700">
API 키별 딘아 필터 통계를 보여 줍니다. 구분 항목으로 시간별,
날짜별, 월별 구분이 가능합니다.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<div>
<select
id="targetTerm"
v-model="targetTerm"
name="targetTerm"
class="mt-0 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
@change="onChange($event)"
>
<option
v-for="term in terms"
:key="term.key"
:selected="term.current"
:value="term.key"
>
<span class="truncate">
{{ term.key }}
</span>
</option>
</select>
</div>
</div>
<Datepicker
v-if="targetTerm == 'year'"
v-model="targetDateYear"
class="mt-4 sm:mt-0 sm:ml-2 sm:flex-none"
locale="ko"
:year-picker="true"
@update:modelValue="handleDate"
></Datepicker>
<Datepicker
v-if="targetTerm == 'month'"
v-model="targetDateMonth"
class="mt-4 sm:mt-0 sm:ml-2 sm:flex-none"
locale="ko"
:month-picker="true"
:format="formatForMonth"
@update:modelValue="handleDate"
></Datepicker>
<Datepicker
v-if="targetTerm == 'day'"
v-model="targetDateDay"
class="mt-4 sm:mt-0 sm:ml-2 sm:flex-none"
locale="ko"
:format="formatForDay"
@update:modelValue="handleDate"
></Datepicker>
</div>
<BaseList1
:headings="listHeadings"
:actions="listActions"
:keys="listKeys"
:data="listData"
:action-key="actionKey"
:column-filter="columnFilter"
:do-action="doAction"
/>
<BasePagination1
:total-page-count="totalPageCount"
:current-page-number="currentPageNumber"
:page-size="pageSize"
:records-total="recordsTotal"
:page-move="pageMove"
/>
<div class="p-0 rounded-bl-2xl rounded-br-2xl md:px-0">
<a
href="javascript:void(0)"
class="text-base font-medium text-indigo-700 hover:text-indigo-600"
@click="$router.back()"
>&larr;이전 화면으로<span aria-hidden="true"> </span
></a>
</div>
</div>
</template>
<script setup lang="ts">
import Datepicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
const router = useRouter();
const { $dayjs } = useNuxtApp();
definePageMeta({
middleware: 'check-auth-op',
});
const targetDate = ref($dayjs(new Date().toISOString()).format('YYYY'));
const targetDateYear = ref($dayjs(new Date().toISOString()).format('YYYY'));
const targetDateMonth = ref({
month: new Date().getMonth(),
year: new Date().getFullYear,
});
const targetDateDay = ref(
$dayjs(new Date().toISOString()).format('MM/DD/YYYY')
);
// console.log('huk = ', targetDate.value);
const format = (date) => {
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
return `${year}${month}${day}`;
};
const formatForDay = (date) => {
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
const hour = date.getHours();
return `${year}${(month < 10 ? '0' : '') + month}${
(day < 10 ? '0' : '') + day
}`;
};
const formatForMonth = (date) => {
return `${date.year}${(date.month + 1 < 10 ? '0' : '') + (date.month + 1)}`;
};
const listHeadings = [
{
title: '구분',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'date_tag',
hiddenInfo: {
headClass: 'font-normal lg:hidden',
dts: [],
dds: [],
},
subClass:
'w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6',
},
{
title: 'word',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
key: 'word',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
},
{
title: 'hit',
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
key: 'count',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
},
{
title: '갱신일',
class: 'px-3 py-3.5 text-left text-sm font-semibold text-gray-900',
key: 'updated',
hiddenInfo: {
headClass: '',
dts: [],
dds: [],
},
subClass: 'px-3 py-4 text-sm text-gray-500',
},
];
const listActions = [];
const actionKey = 'serial';
const listKeys = [
'serial',
'date_tag',
'total',
'hit',
'size',
'uniq_ip',
'uniq_referrer',
'updated',
];
const listData = ref([]);
const totalPageCount = ref(0);
const currentPageNumber = ref(1);
const pageSize = ref(26);
const recordsTotal = ref(0);
// const order = [{ column: 'serial', dir: 'desc' }];
// const columns = { serial: { data: 'serial' } };
function columnFilter(key, val) {
// console.log('columnFilter(), key = ', key, ', val = ', val);
if (key == 'updated' || key == 'created') {
return $dayjs(val).format('YY/MM/DD A h:mm:ss');
// return $dayjs(val).format('YY/MM/DD');
} else if (key == 'size') {
return _utils.formatBytes(val, 2);
} else {
return val;
}
}
function doAction(tag, target) {
console.log('on doAction(), tag=', tag, ', target=', target);
router.push({
name: 'key-edit',
params: { target: target },
});
}
function pageMove(targetPageIdex) {
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function refresh() {
const responseJson = await _crossCtl.doComm(
'local/list',
'admin:statistics:word',
{
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
hero: targetKey.value,
term: targetTerm.value,
// termPrefix: _utils.getDateTimeTag(targetTerm.value.substring(0, 1)),
termPrefix: targetDate.value,
}
);
currentPageNumber.value = responseJson['currentPageNumber'];
totalPageCount.value = responseJson['totalPageCount'];
pageSize.value = parseInt(responseJson['pageSize']);
recordsTotal.value = responseJson['recordsTotal'];
for (let i = 0; i < responseJson['data'].length; i++) {
responseJson['data'][i]['hit_ratio'] =
(
(responseJson['data'][i]['hit'] /
responseJson['data'][i]['total']) *
100
).toFixed(2) + '%';
responseJson['data'][i]['size_avg'] = _utils.formatBytes(
responseJson['data'][i]['size'] / responseJson['data'][i]['total'],
2
);
}
listData.value = responseJson['data'];
console.log('listData.value=', listData.value);
}
const terms = ref([]);
const route = useRoute();
const hero = route.params.hero;
const targetTerm = ref('year');
const targetKey = ref('');
const tmpTerms = [
{ current: targetTerm.value == 'year', key: 'year' },
{ current: targetTerm.value == 'month', key: 'month' },
{ current: targetTerm.value == 'day', key: 'day' },
];
terms.value = tmpTerms;
const responseJson = await _crossCtl.doComm('local/select', 'admin:key', {
hero: hero,
});
if (responseJson['data'].length == 0) {
_crossCtl.openModal(
'error',
'잘못된 파라메타',
'키 정보를 읽어올 수 없습니다.\n확인 버튼을 누르면 메인 화면으로 돌아갑니다.',
['확인'],
(btnIdx) => {
navigateTo('/');
}
);
} else {
targetKey.value = responseJson['data'][0]['api_key'];
}
function handleDate(date) {
console.log('huk date = ', date);
let result = date;
switch (targetTerm.value) {
case 'year':
result = `${date}`;
break;
case 'month':
result = `${date.year}${
(date.month + 1 < 10 ? '0' : '') + (date.month + 1)
}`;
break;
case 'day':
result = $dayjs(date).format('YYYYMMDD');
break;
}
targetDate.value = result;
console.log('huk result = ', result);
refresh();
}
function onChange(e) {
console.log('targetTerm.value=', targetTerm.value);
switch (targetTerm.value) {
case 'year':
targetDate.value = $dayjs(new Date().toISOString()).format('YYYY');
break;
case 'month':
targetDate.value = $dayjs(new Date().toISOString()).format(
'YYYYMM'
);
break;
case 'day':
targetDate.value = $dayjs(new Date().toISOString()).format(
'YYYYMMDD'
);
break;
}
terms.value = [
{ current: targetTerm.value == 'year', key: 'year' },
{ current: targetTerm.value == 'month', key: 'month' },
{ current: targetTerm.value == 'day', key: 'day' },
];
refresh();
}
refresh();
</script>