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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,176 @@
<!--
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">
입력된 문장을 api를 이용해 검사하고 결과를
표시합니다.
</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="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="filterText"
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="filterText"
v-model="filterText"
name="filterText"
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>
<div class="pt-5">
<div class="flex justify-end"></div>
</div>
</form>
</div>
</template>
<script setup lang="ts">
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
definePageMeta({
middleware: 'check-auth-op',
});
const notificationMethods = [
{
id: 'quick',
title: 'mode = quick. 첫번째 매칭이 발견되면 더 이상 검사하지 않고 바로 결과를 리턴',
},
{ id: 'normal', title: 'mode = normal. 전체를 검사하여 모든 매칭을 리턴' },
{
id: 'filter',
title: 'mode = filter. 전체를 검사하며 모든 매칭을 마스크로 치환한 결과까지 리턴',
},
];
const filterText = ref('테스트 할 문장을 이곳에 넣어 주세요.');
const filterMode = ref('filter');
const filterCallback = 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: filterText.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']);
}
}
</script>

View File

@@ -0,0 +1,543 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<div class="mx-auto max-w-3xl text-base leading-7 text-gray-700 px-4">
<h1 class="mt-10 text-3xl font-bold tracking-tight text-gray-900">
KISO 이용자 보호 시스템 API 서비스
</h1>
<p class="mt-2 text-lg leading-8 text-gray-600">
KSS(KISO Safeguard System) API Service
</p>
<div class="mt-10 max-w-2xl">
<h2 class="mt-16 text-2xl font-bold tracking-tight text-gray-900">
KSS 소개
</h2>
<p class="mt-6">
KISO 이용자보호시스템(KSS, KISO Safeguard System) API는 국내
대표 포털 네이버와 카카오로부터 제공받은 욕설·비속어 DB를 활용해
개발되었습니다.
</p>
<p class="mt-6">
해당 API는 80 건의 욕설·비속어 DB를 활용해 입력된 표현
사전에 포함된 단어가 있는지 검사하고 결과를 필터링해 주는
서비스를 제공합니다.
</p>
<p class="mt-6">
서비스의 목적은 다양한 인터넷 서비스에서 사업자가 별도의
욕설·비속어 DB구축 유지 보수의 부담이 없이 욕설·비속어 표현에
대한 서비스 관리 운영에 도움을 드리는 것입니다.
</p>
</div>
<div class="mt-10 max-w-2xl">
<h2 class="mt-16 text-2xl font-bold tracking-tight text-gray-900">
주요 특징
</h2>
<ul role="list" class="mt-8 max-w-xl space-y-8 text-gray-600">
<li class="flex gap-x-3">
<CheckCircleIcon
class="mt-1 h-5 w-5 flex-none text-indigo-600"
aria-hidden="true"
/>
<span
><strong class="font-semibold text-gray-900"
>표준화된 필터 데이터베이스</strong
>
<p>
KSS에서 사용하는 필터 단어 데이터베이스는 네이버와
카카오와 같은 국내 대형 포털로부터 제공받은 기반
데이터를 정제하여 표준화 것입니다. 대부분의
인터넷 커뮤니티 서비스에서 바로 사용하기에 적합
합니다.
</p>
</span>
</li>
<li class="flex gap-x-3">
<CheckCircleIcon
class="mt-1 h-5 w-5 flex-none text-indigo-600"
aria-hidden="true"
/>
<span
><strong class="font-semibold text-gray-900"
>빠른 검색 속도</strong
>
<p>
현재 검색 대상 단어 데이터베이스에는 80만개의 단어가
등록되어 있으며 이는 시간이 흐름에 따라 계속
늘어나게 됩니다. 하지만 고객사의 API 호출시 필터
단어를 검색하는 시간은 데이터베이스 크기와 무관하게
검사 대상인 텍스트의 사이즈만큼만 시간을 소모하도록
만들어져 있어서 대부분의 인터넷 서비스에 사용 적합
합니다.
</p>
</span>
</li>
<li class="flex gap-x-3">
<CheckCircleIcon
class="mt-1 h-5 w-5 flex-none text-indigo-600"
aria-hidden="true"
/>
<span
><strong class="font-semibold text-gray-900"
>안정적인 인프라</strong
>
<p>
KSS는 우수한 성능과 안정성을 보유한 AWS 클라우드를
사용하여 안정적인 서비스를 제공합니다. 서비스
사용량이 많아서 충분한 처리 속도를 확보하기를 원하는
회원사를 위한 부하 격리 기능도 준비되어 있습니다.
</p>
</span>
</li>
<li class="flex gap-x-3">
<CheckCircleIcon
class="mt-1 h-5 w-5 flex-none text-indigo-600"
aria-hidden="true"
/>
<span
><strong class="font-semibold text-gray-900"
>지속적인 업데이트</strong
>
<p>
KSS는 포털 회원사의 지속적인 DB 제공으로 인터넷상
빠르게 전파되는 욕설·비속어에 대응할 있습니다.
DB를 직접 제공하지 않고 응용프로그램 인터페이스(API)
방식으로 서비스하는 것은 업데이트를 하기
위해서입니다.
</p>
</span>
</li>
</ul>
</div>
<div class="mt-10 max-w-2xl">
<h2 class="mt-16 text-2xl font-bold tracking-tight text-gray-900">
필터 DB 생성 절차
</h2>
<div class="mt-6 lg:border-b lg:border-t lg:border-gray-200">
<nav class="mx-auto max-w-7xl px-2" aria-label="Progress">
<ol
role="list"
class="overflow-hidden rounded-md lg:flex lg:rounded-none lg:border-l lg:border-r lg:border-gray-200"
>
<li
v-for="(step, stepIdx) in steps_db"
:key="step.id"
class="relative overflow-hidden lg:flex-1"
>
<div
:class="[
stepIdx === 0
? 'rounded-t-md border-b-0'
: '',
stepIdx === steps.length - 1
? 'rounded-b-md border-t-0'
: '',
'overflow-hidden border border-gray-200 lg:border-0',
]"
>
<a :href="step.href" aria-current="step">
<span
class="absolute left-0 top-0 h-full w-1 bg-indigo-600 lg:bottom-0 lg:top-auto lg:h-1 lg:w-full"
aria-hidden="true"
/>
<span
:class="[
stepIdx !== 0 ? '' : '',
'flex items-start px-5 py-5 text-sm font-medium',
]"
>
<span class="flex-shrink-0">
<span
class="flex h-10 w-10 items-center justify-center rounded-full border-2 border-indigo-600"
>
<span class="text-indigo-600">{{
step.id
}}</span>
</span>
</span>
<span
class="lg:text-center ml-2 mt-0.5 flex min-w-0 flex-col"
>
<span
class="text-sm font-medium text-indigo-600"
>{{ step.name }}</span
>
<span
class="text-sm font-medium text-gray-500"
>{{ step.description }}</span
>
</span>
</span>
</a>
<template v-if="stepIdx !== 0">
<!-- Separator -->
<div
class="absolute inset-0 left-0 top-0 hidden w-3 lg:block"
aria-hidden="true"
>
<svg
class="h-full w-full text-gray-300"
viewBox="0 0 12 82"
fill="none"
preserveAspectRatio="none"
>
<path
d="M0.5 0V31L10.5 41L0.5 51V82"
stroke="currentcolor"
vector-effect="non-scaling-stroke"
/>
</svg>
</div>
</template>
</div>
</li>
</ol>
</nav>
</div>
<div></div>
<p class="mt-6 text-sm">
과정을 일정 기간마다 주기적으로 수행하여 항상 최신의 표준화된
필터 DB를 서비스 합니다.
</p>
</div>
<div class="mt-10 max-w-2xl">
<h2 class="mt-16 text-2xl font-bold tracking-tight text-gray-900">
서비스 비용
</h2>
<p class="mt-6">
KSS API 서비스는 회원사, 언론사, 공공기관에 무료로 이용
가능하도록 배포하고 있습니다. KSS 만을 이용하는 경우 서비스
이용료( 6만원) 발생 됩니다.
</p>
<div class="bg-white py-1 rounded-2xl">
<div class="mx-auto max-w-7xl px-2">
<div class="mx-auto max-w-2xl lg:max-w-none">
<dl
class="mt-1 mb-1 grid grid-cols-1 gap-0.5 overflow-hidden rounded-2xl text-center sm:grid-cols-2 lg:grid-cols-4"
>
<div
v-for="stat in stats"
:key="stat.id"
class="flex flex-col bg-gray-400/5 py-6"
>
<dt
class="text-2sm font-semibold leading-6 text-gray-600"
>
{{ stat.name }}
</dt>
<dd
class="order-first text-xl font-semibold tracking-tight text-gray-900"
>
{{ stat.value }}
</dd>
</div>
</dl>
</div>
</div>
</div>
<p class="mt-6 text-sm">
* 서비스 제공 내용과 요금은 향후 변동 가능하므로 공지 사항이나
이메일 공지를 반드시 확인해 주세요.
</p>
</div>
<div class="mt-10 max-w-2xl">
<h2
id="service-use-agreement"
class="mt-16 text-2xl font-bold tracking-tight text-gray-900"
>
서비스 이용 절차
</h2>
<nav class="mt-6 ml-6" aria-label="Progress">
<ol role="list" class="overflow-hidden">
<li
v-for="(step, stepIdx) in steps"
:key="step.name"
:class="[
stepIdx !== steps.length - 1 ? 'pb-10' : '',
'relative',
]"
>
<template v-if="step.status === 'complete'">
<div
v-if="stepIdx !== steps.length - 1"
class="absolute left-4 top-4 -ml-px mt-0.5 h-full w-0.5 bg-indigo-600"
aria-hidden="true"
/>
<a
:href="step.href"
class="group relative flex items-start"
>
<span class="flex h-9 items-center">
<span
class="relative z-10 flex h-8 w-8 items-center justify-center rounded-full bg-indigo-600 group-hover:bg-indigo-800"
>
<CheckIcon
class="h-5 w-5 text-white"
aria-hidden="true"
/>
</span>
</span>
<span class="ml-4 flex min-w-0 flex-col">
<span class="text-sm font-medium">{{
step.name
}}</span>
<span class="text-sm text-gray-500">{{
step.description
}}</span>
</span>
</a>
</template>
<template v-else-if="step.status === 'current'">
<div
v-if="stepIdx !== steps.length - 1"
class="absolute left-4 top-4 -ml-px mt-0.5 h-full w-0.5 bg-gray-300"
aria-hidden="true"
/>
<a
:href="step.href"
class="group relative flex items-start"
aria-current="step"
>
<span
class="flex h-9 items-center"
aria-hidden="true"
>
<span
class="relative z-10 flex h-8 w-8 items-center justify-center rounded-full border-2 border-indigo-600 bg-white"
>
<span
class="h-2.5 w-2.5 rounded-full bg-indigo-600"
/>
</span>
</span>
<span class="ml-4 flex min-w-0 flex-col">
<span
class="text-sm font-medium text-indigo-600"
>{{ step.name }}</span
>
<span class="text-sm text-gray-500">{{
step.description
}}</span>
</span>
</a>
</template>
<template v-else>
<div
v-if="stepIdx !== steps.length - 1"
class="absolute left-4 top-4 -ml-px mt-0.5 h-full w-0.5 bg-gray-300"
aria-hidden="true"
/>
<a
:href="step.href"
class="group relative flex items-start"
>
<span
class="flex h-9 items-center"
aria-hidden="true"
>
<span
class="relative z-10 flex h-8 w-8 items-center justify-center rounded-full border-2 border-gray-300 bg-white group-hover:border-gray-400"
>
<span
class="h-2.5 w-2.5 rounded-full bg-transparent group-hover:bg-gray-300"
/>
</span>
</span>
<span class="ml-4 flex min-w-0 flex-col">
<span
class="text-sm font-medium text-gray-500"
>{{ step.name }}</span
>
<span class="text-sm text-gray-500">{{
step.description
}}</span>
</span>
</a>
</template>
</li>
</ol>
</nav>
</div>
<div class="mt-10 max-w-2xl">
<h2 class="mt-16 text-2xl font-bold tracking-tight text-gray-900">
사용문의
</h2>
<p class="mt-6">
API 사용에 필요한 자세한 기술 문서는
<a
class="font-semibold text-indigo-600 hover:text-indigo-500"
href="javascript:void()"
@click="navigateTo('/doc/api_doc')"
>API 연동 안내</a
>
수록되어 있습니다. 또한
<a
class="font-semibold text-indigo-600 hover:text-indigo-500"
href="javascript:void()"
@click="navigateTo('/doc/guide')"
>서비스 도입 안내</a
>,
<a
class="font-semibold text-indigo-600 hover:text-indigo-500"
href="javascript:void()"
@click="navigateTo('/support/faq')"
>FAQ
</a>
통해 KSS API 서비스 개념과 사용방법에 대해 살펴보시기
바랍니다.
</p>
<p class="mt-6">
API 연동 관련 문의는 이메일(netsafe@kiso.or.kr) 보내주시기
바랍니다.
</p>
</div>
<div class="mt-10 max-w-2xl">
<h2
class="mt-16 text-2xl font-bold tracking-tight text-gray-900"
></h2>
<p class="mt-6"></p>
</div>
<div class="py-1">
<div class="mx-auto max-w-7xl px-6">
<div class="mx-auto max-w-2xl">
<div
class="mx-auto mt-2 mb-2 grid grid-cols-4 items-start gap-x-8 gap-y-10"
>
<img
class="col-span-2 max-h-12 w-full object-contain object-left"
src="https://dev.safekiso.com/kiso_ci_1.png"
alt="Transistor"
width="158"
height="48"
/>
<img
class="col-span-2 max-h-12 w-full object-contain object-left"
src="https://www.safekiso.com/logos/kss_certification_logo_box_english.png"
alt="Reform"
width="158"
height="48"
/>
</div>
</div>
</div>
</div>
<div class="mt-10 max-w-2xl">
<h2
class="mt-16 text-2xl font-bold tracking-tight text-gray-900"
></h2>
<p class="mt-6"></p>
</div>
</div>
</template>
<script lang="ts" setup>
import {
CheckCircleIcon,
InformationCircleIcon,
} from '@heroicons/vue/20/solid';
import { CheckIcon } from '@heroicons/vue/20/solid';
const steps_db = [
{
id: '01',
name: '네이버, 카카오',
description: '욕설·비속어 DB 제공',
href: '#',
status: 'current',
},
{
id: '02',
name: 'KISO',
description: '표준화, DB 구축',
href: '#',
status: 'current',
},
{
id: '03',
name: 'KSS API',
description: 'API 서비스 제공',
href: '#',
status: 'current',
},
];
const steps = [
{
name: '서비스 사용 신청',
description: '이 메일을 통한 서비스 등록 신청',
href: '#',
status: 'complete',
},
{
name: '내부 검토 및 계정 등록',
description: '고객사의 회원 자격 등을 검토 하여 계정을 등록 합니다.',
href: '#',
status: 'current',
},
{
name: '회원 가입',
description: '등록된 이메일 주소로 회원 가입을 진행 합니다.',
href: '#',
status: 'upcoming',
},
{
name: 'API 키 생성',
description: '웹 어드민 기능을 통해 필요한 API 키를 생성 합니다.',
href: '#',
status: 'upcoming',
},
{
name: '서비스 연동',
description:
'연동 가이드 등을 참고로 발급된 API 키를 서비스와 연동 합니다.',
href: '#',
status: 'upcoming',
},
];
const includedFeatures = [
'최신 통합 필터 DB 적용',
'일 최대 8백만건 호출',
'초당 최대 100회 호출',
'웹 어드민 페이지',
];
const stats = [
{ id: 1, name: '일 최대 호출', value: '8,640,000' },
{ id: 2, name: '1초 호출 제한', value: '100회' },
{ id: 3, name: '관리 기능 제공', value: '웹 어드민' },
{ id: 4, name: '낮은 도입 부담', value: '월 단위 결제' },
];
const route = useRoute();
console.log('route.query = ', route.query);
console.log('route = ', route.fullPath);
onMounted(() => {
console.log('myheader mounted');
if (route.fullPath.endsWith('#service-use-agreement')) {
const container = document.getElementById('service-use-agreement');
container.scrollIntoView({ behavior: 'smooth' });
}
});
</script>

View File

@@ -0,0 +1,451 @@
<template>
<div class="m-0">
<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-0 space-y-6 sm:pt-0 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>
<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="gotoStatistics()"
>
사용 통계
</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="gotoWordStatistics()"
>
단어 통계
</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"
disabled
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"
disabled
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-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="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>
<div class="divide-y divide-gray-200 space-x-8"></div>
</form>
<form
class="space-y-6 pt-8 sm:pt-10 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">
입력된 문장을 api를 이용해 검사하고 결과를
표시합니다.
</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="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="filterText"
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="filterText"
v-model="filterText"
name="filterText"
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>
<div class="pt-5">
<div class="flex justify-end"></div>
</div>
</form>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const router = useRouter();
definePageMeta({
middleware: 'check-auth-op',
});
const hero = route.params.hero;
console.log('hero=', hero);
const apiKey = ref('');
const name = ref('');
const level = ref('');
const status = ref(0);
const notificationMethods = [
{
id: 'quick',
title: 'mode = quick. 첫번째 매칭이 발견되면 더 이상 검사하지 않고 바로 결과를 리턴',
},
{ id: 'normal', title: 'mode = normal. 전체를 검사하여 모든 매칭을 리턴' },
{
id: 'filter',
title: 'mode = filter. 전체를 검사하며 모든 매칭을 마스크로 치환한 결과까지 리턴',
},
];
const filterText = ref('테스트할 문장을 여기에 입력해 주세요.');
const filterMode = ref('filter');
const filterCallback = 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.doFilterRaw(apiKey.value, {
text: filterText.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']);
}
}
function gotoStatistics() {
navigateTo('/key/' + hero + '/statistics');
}
function gotoWordStatistics() {
navigateTo('/word/' + hero + '/statistics');
}
const responseJson = await _crossCtl.doComm('local/select', 'key', {
hero: hero,
});
if (responseJson['data'].length == 0) {
_crossCtl.openModal(
'error',
'잘못된 파라메타',
'키 정보를 읽어올 수 없습니다.\n확인 버튼을 누르면 메인 화면으로 돌아갑니다.',
['확인'],
(btnIdx) => {
navigateTo('/');
}
);
} else {
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', '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', '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', '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,265 @@
<!-- 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.uid;
console.log('targetName=', targetName);
console.log('hero=', hero);
let listTarget = 'log:user:active';
const listHeadings = [
{
title: '누가',
class: 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6',
key: 'uid',
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: '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);
} else if (tag == 'search') {
console.log('search for ', target);
if (target == '') {
listTarget = 'log:user:active';
} else {
listTarget = 'log:user';
// hero = target;
}
refresh();
}
}
function pageMove(targetPageIdex) {
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
currentPageNumber.value = targetPageIdex;
refresh();
}
async function refresh() {
const responseJson = await _crossCtl.doComm('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'];
}
if (hero != undefined) {
doAction('search', hero);
} else {
refresh();
}
</script>

View File

@@ -0,0 +1,433 @@
<!-- 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()));
// 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', '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', '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,175 @@
<!-- 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">
<button
type="button"
class="inline-flex mr-3 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-red-400 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 sm:w-auto"
@click="$router.push('/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-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: '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('/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',
'key:deleted',
{
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,425 @@
<template>
<div class="m-0">
<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-0 space-y-6 sm:pt-0 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>
<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="gotoStatistics()"
>
통계 보기
</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-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="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>
<div class="divide-y divide-gray-200 space-x-8"></div>
</form>
<form
class="space-y-6 pt-8 sm:pt-10 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">
입력된 문장을 api를 이용해 검사하고 결과를
표시합니다.
</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="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="filterText"
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="filterText"
v-model="filterText"
name="filterText"
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>
<div class="pt-5">
<div class="flex justify-end"></div>
</div>
</form>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const router = useRouter();
definePageMeta({
middleware: 'check-auth-op',
});
const hero = route.params.target;
console.log('hero=', hero);
const apiKey = ref('');
const name = ref('');
const level = ref('');
const status = ref(0);
const notificationMethods = [
{
id: 'quick',
title: 'mode = quick. 첫번째 매칭이 발견되면 더 이상 검사하지 않고 바로 결과를 리턴',
},
{ id: 'normal', title: 'mode = normal. 전체를 검사하여 모든 매칭을 리턴' },
{
id: 'filter',
title: 'mode = filter. 전체를 검사하며 모든 매칭을 마스크로 치환한 결과까지 리턴',
},
];
const filterText = ref('테스트할 문장을 여기에 입력해 주세요.');
const filterMode = ref('filter');
const filterCallback = 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.doFilterRaw(apiKey.value, {
text: filterText.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']);
}
}
function gotoStatistics() {
navigateTo('/key/' + hero + '/statistics/year');
}
const responseJson = await _crossCtl.doComm('local/select', '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', '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', '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', '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,194 @@
<!-- 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 키를 보고 관리할 있습니다. ({{ recordsTotal }} /
{{ limitCount }})
</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('/key/deleted')"
>
삭제 리스트
</button>
<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="maekNewKey"
>
만들기
</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-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: '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 limitCount = ref(5);
// 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() {
if (recordsTotal.value >= limitCount.value) {
alert(
'키 생성 갯수 제한을 초과 할 수 없습니다. 관리자에게 문의하세요.'
);
} else {
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('/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',
'key:active',
{
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'];
limitCount.value = responseJson['metaData']['limitCount'];
console.log('listData.value=', listData.value);
}
}
refresh();
</script>

View File

@@ -0,0 +1,206 @@
<!--
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>
<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="first-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="first-name"
v-model="name"
type="text"
name="first-name"
autocomplete="given-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="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"
disabled
class="opacity-75 focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-mid"
class="opacity-75 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"
disabled
class="opacity-75 focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
<label
for="filter-low"
class="opacity-75 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-op',
});
const name = ref('');
const level = ref('high');
async function doCancel() {
router.back();
}
watch(level, (newValue, oldValue) => {
console.log('level 의 변이가 감지되었을 때 ', {
newValue,
oldValue,
});
});
async function doCreate() {
const responseJson = await _crossCtl.doComm('local/insert', 'key', {
name: name.value,
level: level.value,
});
console.log('responseJson=', responseJson);
if (responseJson['responseMessage'] == 'ok') {
alert('ok');
router.back();
} else {
if (responseJson['responseMessage'] == 'exceed limit') {
alert(
'키 생성 갯수 제한을 초과 할 수 없습니다. 관리자에게 문의하세요.'
);
} else {
responseJson['responseMessage'];
}
}
}
</script>

View File

@@ -0,0 +1,47 @@
<!-- 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>
<br />
<nav class="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: '일별 통계', href: '/key/statistics/daily' },
{ name: '월별 통계', href: '/key/statistics/monthly' },
];
</script>

View File

@@ -0,0 +1,276 @@
<!-- 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>현재 통계 범위 : {{ targetTerm }}</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">
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: 'api_key',
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',
'api_key',
'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 if (key == 'api_key') {
if (keyNameMap[val] != undefined) {
return keyNameMap[val];
} else {
return 'invailed_key';
}
} 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',
'statistics:month',
{
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.value,
hero: targetTerm.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 targetTerm = ref(_utils.getDateTimeTag('m'));
const terms = ref([]);
const keyNameMap = {};
const responseJson = await _crossCtl.doComm('local/list', 'key', {
start: 0,
length: -1,
});
const tmpTerms = [];
for (let i = 0; i < responseJson['data'].length; i++) {
keyNameMap[responseJson['data'][i]['api_key']] =
responseJson['data'][i]['name'];
}
console.log('tmpTerms=', tmpTerms);
terms.value = tmpTerms;
function onChangeTerm(e) {
console.log('targetTerm.value=', targetTerm.value);
refresh();
}
refresh();
</script>

View File

@@ -0,0 +1,413 @@
<!-- 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()));
// 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', 'statistics', {
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.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('month');
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;
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,245 @@
<!-- 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>
<div class="mt-5 bg-gray-100">
<div class="pt-12 sm:pt-16 lg:pt-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center">
<h2
class="text-3xl font-extrabold text-gray-900 sm:text-4xl lg:text-5xl"
>
이번달 사용량과 예상 비용
</h2>
<p class="mt-4 text-xl text-gray-600">
페이지를 보고 있는 시점까지 가공된 로그를
기준으로 사용량을 판정하여 비용을 예상해 보여
드립니다.
</p>
</div>
</div>
</div>
<div class="mt-8 bg-white pb-16 sm:mt-12 sm:pb-20 lg:pb-28">
<div class="relative">
<div class="absolute inset-0 h-1/2 bg-gray-100" />
<div
class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
>
<div
class="max-w-lg mx-auto rounded-lg shadow-lg overflow-hidden lg:max-w-none lg:flex"
>
<div class="flex-1 bg-white px-6 py-8 lg:p-12">
<h3
class="text-2xl font-extrabold text-gray-900 sm:text-3xl"
>
실시간 사용량
</h3>
<ul
role="list"
class="mt-8 space-y-5 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-x-8 lg:gap-y-5"
>
<li class="flex items-start lg:col-span-1">
<div class="flex-shrink-0">
<CheckCircleIcon
class="h-5 w-5 text-green-400"
aria-hidden="true"
/>
</div>
<p class="ml-1 text-sm text-gray-700">
호출 :
{{
_utils.formatNumberInBytesStyle(
total,
2
)
}}
</p>
</li>
<li class="flex items-start lg:col-span-1">
<div class="flex-shrink-0">
<CheckCircleIcon
class="h-5 w-5 text-green-400"
aria-hidden="true"
/>
</div>
<p class="ml-3 text-sm text-gray-700">
검출 :
{{
_utils.formatNumberInBytesStyle(
hit,
2
)
}}
</p>
</li>
<li class="flex items-start lg:col-span-1">
<div class="flex-shrink-0">
<CheckCircleIcon
class="h-5 w-5 text-green-400"
aria-hidden="true"
/>
</div>
<p class="ml-1 text-sm text-gray-700">
전송량 :
{{ _utils.formatBytes(size, 2) }}
</p>
</li>
<li class="flex items-start lg:col-span-1">
<div class="flex-shrink-0">
<CheckCircleIcon
class="h-5 w-5 text-green-400"
aria-hidden="true"
/>
</div>
<p class="ml-1 text-sm text-gray-700">
검출율 :
{{
(hitRatio * 100).toFixed(2) +
'%'
}}
</p>
</li>
</ul>
<div class="mt-8">
<ul>
<li
class="flex items-start lg:col-span-1"
>
<div class="flex-shrink-0">
<CheckCircleIcon
class="h-5 w-5 text-green-400"
aria-hidden="true"
/>
</div>
<p
class="ml-1 text-sm text-gray-700"
>
평균 전송량 :
{{
_utils.formatBytes(
avgSize,
2
)
}}
</p>
</li>
</ul>
</div>
</div>
<div
class="py-8 px-6 text-center bg-gray-50 lg:flex-shrink-0 lg:flex lg:flex-col lg:justify-center lg:p-12"
>
<p
class="text-lg leading-6 font-medium text-gray-900"
>
예상 요금
</p>
<div
class="mt-4 flex items-center justify-center text-4xl font-extrabold text-gray-900"
>
<span>
{{
_utils.formatNumberWithComma(
(total - 10 < 0
? 0
: total - 10) * 1
)
}}
</span>
<span
class="ml-3 text-xl font-medium text-gray-500"
>
</span>
</div>
<p class="mt-4 text-sm">
<a
href="javascript:void(0)"
class="font-medium text-gray-500 underline"
@click="navigateTo('/doc/bill')"
>
요금 계산 기준 보기
</a>
</p>
<div class="mt-6"></div>
<div class="mt-4 text-sm"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<nav class="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">
import { CheckCircleIcon } from '@heroicons/vue/24/solid';
definePageMeta({
middleware: 'check-auth-op',
});
const total = ref(0);
const hit = ref(0);
const size = ref(0);
const hitRatio = ref(0);
const avgSize = ref(0);
const includedFeatures = [
'Private forum access',
'Member resources',
'Entry to annual conference',
'Official member t-shirt',
];
const navigation = [{ name: '상세 통계 보기', href: '/key/statistics/detail' }];
const responseJson = await _crossCtl.doComm('local/list', 'bill:month', {
start: 0,
length: 1,
});
if (responseJson['data'].length == 1) {
total.value = responseJson['data'][0]['total'];
hit.value = responseJson['data'][0]['hit'];
size.value = responseJson['data'][0]['size'];
hitRatio.value =
responseJson['data'][0]['hit'] / responseJson['data'][0]['total'];
avgSize.value =
responseJson['data'][0]['size'] / responseJson['data'][0]['total'];
}
console.log('huk responseJson = ', responseJson);
</script>

View File

@@ -0,0 +1,38 @@
<!-- 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">
선택된 키의 필터 기능을 테스트 합니다.
<br />
검사할 텍스트
</p>
<br />
있는 페이지 :
<a href="javascript:void(0)" @click="$router.push('/key/list')">
API 리스트
</a>
,
<a href="javascript:void(0)" @click="$router.push('/')"> </a>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'check-auth-op',
});
</script>

View File

@@ -0,0 +1,415 @@
<!-- 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()));
// 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', 'statistics:my', {
start: (currentPageNumber.value - 1) * pageSize.value,
length: pageSize.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 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;
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,358 @@
<!-- 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">
필터된 단어 통계 상세 보기 (Top 10 only)
</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"
/>
<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',
'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', '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>