first
This commit is contained in:
20
inspond-nuxt-safekiso/.editorconfig
Normal file
20
inspond-nuxt-safekiso/.editorconfig
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
# Change these settings to your own preference
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# I recommend you to keep these unchanged
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
4
inspond-nuxt-safekiso/.eslintignore
Normal file
4
inspond-nuxt-safekiso/.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# don't ever lint node_modules
|
||||||
|
node_modules
|
||||||
|
# don't lint build output (make sure it's set to your correct build folder name)
|
||||||
|
.output
|
||||||
33
inspond-nuxt-safekiso/.eslintrc.js
Normal file
33
inspond-nuxt-safekiso/.eslintrc.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: 'vue-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
// "plugin:vue/strongly-recommended",
|
||||||
|
// 'eslint:recommended',
|
||||||
|
'plugin:vue/vue3-recommended',
|
||||||
|
'@vue/typescript/recommended',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
plugins: ['@typescript-eslint', 'prettier'],
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||||
|
|
||||||
|
// not needed for vue 3
|
||||||
|
'vue/no-multiple-template-root': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['layouts/default.vue', 'error.vue', 'pages/index.vue'],
|
||||||
|
rules: { 'vue/multi-word-component-names': 'off' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
21
inspond-nuxt-safekiso/.gitattributes
vendored
Normal file
21
inspond-nuxt-safekiso/.gitattributes
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
*.cs text diff=csharp
|
||||||
|
*.java text diff=java
|
||||||
|
*.html text diff=html
|
||||||
|
*.css text
|
||||||
|
*.js text
|
||||||
|
*.sql text
|
||||||
|
|
||||||
|
*.csproj text merge=union
|
||||||
|
*.sln text merge=union eol=crlf
|
||||||
|
|
||||||
|
*.docx diff=astextplain
|
||||||
|
*.DOCX diff=astextplain
|
||||||
|
|
||||||
|
# absolute paths are ok, as are globs
|
||||||
|
/**/postinst* text eol-lf
|
||||||
|
|
||||||
|
# paths that don't start with / are treated relative to the .gitattributes folder
|
||||||
|
relative/path/*.txt text eol-lf
|
||||||
8
inspond-nuxt-safekiso/.gitignore
vendored
Normal file
8
inspond-nuxt-safekiso/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
*.log*
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
.output
|
||||||
|
.env
|
||||||
|
dist
|
||||||
7
inspond-nuxt-safekiso/.prettierrc
Normal file
7
inspond-nuxt-safekiso/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
||||||
|
|
||||||
42
inspond-nuxt-safekiso/README.md
Normal file
42
inspond-nuxt-safekiso/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Nuxt 3 Minimal Starter
|
||||||
|
|
||||||
|
Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install --shamefully-hoist
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on http://localhost:3000
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.
|
||||||
3
inspond-nuxt-safekiso/base/assets/css/tailwind.pcss
Normal file
3
inspond-nuxt-safekiso/base/assets/css/tailwind.pcss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
157
inspond-nuxt-safekiso/base/components/BaseAttachmentCtl1.vue
Normal file
157
inspond-nuxt-safekiso/base/components/BaseAttachmentCtl1.vue
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
(attachments.length > 0 && readOnlyFlag == true) ||
|
||||||
|
readOnlyFlag == false
|
||||||
|
"
|
||||||
|
class="mt-3 sm:col-span-2"
|
||||||
|
>
|
||||||
|
<dt class="text-sm font-medium text-gray-500">
|
||||||
|
<p v-if="readOnlyFlag" class="font-medium">첨부된 파일</p>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="font-medium text-blue-600 hover:text-blue-500"
|
||||||
|
@click="addFiles()"
|
||||||
|
>
|
||||||
|
파일 첨부
|
||||||
|
</a>
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm text-gray-900">
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
class="border border-gray-200 rounded-md divide-y divide-gray-200"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="attachment in attachments"
|
||||||
|
:key="attachment.name"
|
||||||
|
class="pl-3 pr-4 py-3 flex items-center justify-between text-sm"
|
||||||
|
>
|
||||||
|
<div class="w-0 flex-1 flex items-center">
|
||||||
|
<PaperClipIcon
|
||||||
|
class="flex-shrink-0 h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span class="ml-2 flex-1 w-0 truncate">
|
||||||
|
{{ attachment.name }} {{ ', ' }}
|
||||||
|
{{ _utils.formatBytes(attachment.size, 2) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="readOnlyFlag" class="ml-4 flex-shrink-0">
|
||||||
|
<a
|
||||||
|
:href="
|
||||||
|
_crossCtl.config['API_BASE_URL'].replace(
|
||||||
|
'/api/',
|
||||||
|
''
|
||||||
|
) + attachment.localUrl
|
||||||
|
"
|
||||||
|
:download="attachment.name"
|
||||||
|
target="_blank"
|
||||||
|
class="font-medium text-blue-600 hover:text-blue-500"
|
||||||
|
>
|
||||||
|
다운로드
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-else class="ml-4 flex-shrink-0">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="font-medium text-red-600 hover:text-red-500"
|
||||||
|
@click="rmvFile(attachment)"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { stringLiteral } from '@babel/types';
|
||||||
|
import { PaperClipIcon } from '@heroicons/vue/24/solid';
|
||||||
|
const props = defineProps({
|
||||||
|
attachments: {
|
||||||
|
type: Array<{ name: ''; localUrl: ''; size: 0; type: 'text/html' }>,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
readOnlyFlag: { type: Boolean, required: true },
|
||||||
|
updateAttachments: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default: () => {
|
||||||
|
void 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
boardId: { type: String, required: false, default: null },
|
||||||
|
secureEnabled: { type: Boolean, required: false, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log('huk props = ', props);
|
||||||
|
|
||||||
|
const currentDomain = ref('');
|
||||||
|
|
||||||
|
if (process.client) {
|
||||||
|
currentDomain.value = _utils.getDomain(window.location.href);
|
||||||
|
|
||||||
|
console.log('currentDomain.value=', currentDomain.value);
|
||||||
|
} else {
|
||||||
|
console.log('server?');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFiles() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.setAttribute('type', 'file');
|
||||||
|
input.setAttribute('multiple', 'multiple');
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
|
||||||
|
// Listen upload local image and save to server
|
||||||
|
input.onchange = () => {
|
||||||
|
if (input.files.length > 0) {
|
||||||
|
console.log('we got file(s) : ', input.files);
|
||||||
|
uploadFiles(input.files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFiles(files) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
formData.append('upload-file', files[i], files[i].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append('target', 'just');
|
||||||
|
if (props.boardId != null) {
|
||||||
|
formData.append('attachedTo', props.boardId);
|
||||||
|
}
|
||||||
|
formData.append('secureEnabled', props.secureEnabled.toString());
|
||||||
|
|
||||||
|
console.log('formData=', formData);
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doUpload('just', formData);
|
||||||
|
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
props.updateAttachments([
|
||||||
|
...props.attachments,
|
||||||
|
...responseJson['files'],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
alert('upload error : ' + responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rmvFile(target) {
|
||||||
|
const tmpAry = [];
|
||||||
|
for (let i = 0; i < props.attachments.length; i++) {
|
||||||
|
if (props.attachments[i] != target) {
|
||||||
|
tmpAry.push(props.attachments[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
props.updateAttachments(tmpAry);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
40
inspond-nuxt-safekiso/base/components/BaseAvater1.vue
Normal file
40
inspond-nuxt-safekiso/base/components/BaseAvater1.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
v-if="photoUrl == ''"
|
||||||
|
class="inline-block rounded-full overflow-hidden bg-gray-100"
|
||||||
|
:style="'height:' + photoSize + 'rem; width:' + photoSize + 'rem'"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-full w-full text-gray-300"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="'inline-block rounded-full border'"
|
||||||
|
:style="'height:' + photoSize + 'rem; width:' + photoSize + 'rem'"
|
||||||
|
:src="photoUrl"
|
||||||
|
:alt="imageAlt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
imageSize: { type: Number, default: 12 },
|
||||||
|
imageAlt: { type: String, default: '' },
|
||||||
|
imageUrl: { type: String, default: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const photoSize = ref(props.imageSize);
|
||||||
|
const photoUrl = ref(props.imageUrl);
|
||||||
|
const imageAlt = ref(props.imageAlt);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
||||||
139
inspond-nuxt-safekiso/base/components/BaseBoardList1.vue
Normal file
139
inspond-nuxt-safekiso/base/components/BaseBoardList1.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div
|
||||||
|
class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<table class="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="(heading, index) in headings"
|
||||||
|
:key="index"
|
||||||
|
:width="
|
||||||
|
heading['widthRatio'] != ''
|
||||||
|
? heading['widthRatio'] + '%'
|
||||||
|
: '100%'
|
||||||
|
"
|
||||||
|
:class="
|
||||||
|
index == 0
|
||||||
|
? 'whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 md:pl-0'
|
||||||
|
: 'whitespace-nowrap py-3.5 px-3 text-left text-sm font-semibold text-gray-900'
|
||||||
|
"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
{{ heading['title'] }}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index + headings.length"
|
||||||
|
:width="actions.length * 1"
|
||||||
|
class="relative py-3.5 pl-3 pr-4 sm:pr-6 md:pr-0"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ action }}</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
<tr v-for="(item, itemIndex) in data" :key="itemIndex">
|
||||||
|
<td
|
||||||
|
v-for="(heading, index) in headings"
|
||||||
|
:key="index"
|
||||||
|
:class="
|
||||||
|
index == 0
|
||||||
|
? 'max-w-0 whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 md:pl-0'
|
||||||
|
: 'w-full whitespace-nowrap py-4 px-3 text-sm text-gray-500'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div v-if="index == 0" class="">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="hover:bg-gray-50"
|
||||||
|
@click="
|
||||||
|
doAction('보기', item[actionKey])
|
||||||
|
"
|
||||||
|
><div class="truncate">
|
||||||
|
{{
|
||||||
|
columnFilter
|
||||||
|
? columnFilter(
|
||||||
|
heading['key'],
|
||||||
|
item[heading['key']]
|
||||||
|
)
|
||||||
|
: item[heading['key']]
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{
|
||||||
|
columnFilter
|
||||||
|
? columnFilter(
|
||||||
|
heading['key'],
|
||||||
|
item[heading['key']]
|
||||||
|
)
|
||||||
|
: item[heading['key']]
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td
|
||||||
|
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 md:pr-0"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="index != 0 ? 'ml-3' : ''"
|
||||||
|
class="text-indigo-600 hover:text-indigo-900"
|
||||||
|
@click="doAction(action, item[actionKey])"
|
||||||
|
>{{ action
|
||||||
|
}}<span class="sr-only"
|
||||||
|
>, {{ item['serial'] }}</span
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="data.length == 0">
|
||||||
|
<td
|
||||||
|
:colspan="headings.length"
|
||||||
|
class="whitespace-nowrap py-4 px-3 text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
{{ 'No Data to display...' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const people = [
|
||||||
|
{
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com 아주 긴 아지 긴 아주 긴 이런 저런 긴 길 저런 길 갈 골 갈',
|
||||||
|
role: 'Member',
|
||||||
|
},
|
||||||
|
// More people...
|
||||||
|
];
|
||||||
|
const props = defineProps({
|
||||||
|
headings: { type: Array<object>, required: true },
|
||||||
|
actions: { type: Array<string>, default: [] },
|
||||||
|
data: { type: Array<object>, required: true },
|
||||||
|
actionKey: { type: String, default: 'serial' },
|
||||||
|
columnFilter: {
|
||||||
|
type: Function,
|
||||||
|
default: (key: string, val: string) => val,
|
||||||
|
},
|
||||||
|
doAction: {
|
||||||
|
type: Function,
|
||||||
|
default: (key: string) => {
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
73
inspond-nuxt-safekiso/base/components/BaseBoardView1.vue
Normal file
73
inspond-nuxt-safekiso/base/components/BaseBoardView1.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<div
|
||||||
|
class="px-4 py-5 sm:px-6 flex justify-between items-center flex-wrap sm:flex-nowrap"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 inline-flex rounded-full border-2 border-white"
|
||||||
|
>
|
||||||
|
<BaseAvater1
|
||||||
|
:image-size="2"
|
||||||
|
:image-url="profileUrl"
|
||||||
|
:image-alt="name + '의 프로필 사진'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<div class="text-base font-medium">{{ name }}</div>
|
||||||
|
<div class="text-base text-xs">{{ created }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-200 px-4 py-5 sm:px-6 min-h-[15rem]">
|
||||||
|
<div
|
||||||
|
ref="myCoolDiv"
|
||||||
|
class="prose mt-3 space-y-0 max-w-none"
|
||||||
|
v-html="content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: { type: String, required: true },
|
||||||
|
profileUrl: { type: String, required: true },
|
||||||
|
title: { type: String, required: true },
|
||||||
|
content: { type: String, required: true },
|
||||||
|
flags: { type: Array<string>, default: [] },
|
||||||
|
attachments: { type: Array<string>, default: [] },
|
||||||
|
hitCount: { type: Number, required: true },
|
||||||
|
likeCount: { type: Number, required: true },
|
||||||
|
dislikeCount: { type: Number, required: true },
|
||||||
|
commentCount: { type: Number, required: true },
|
||||||
|
reportCount: { type: Number, required: true },
|
||||||
|
status: { type: Number, required: true },
|
||||||
|
updated: { type: String, required: true },
|
||||||
|
created: { type: String, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const myCoolDiv = ref(null);
|
||||||
|
|
||||||
|
watch(myCoolDiv, () => {
|
||||||
|
if (myCoolDiv.value.childNodes[0].tagName == 'IFRAME') {
|
||||||
|
myCoolDiv.value.childNodes[0].classList.add('xl:w-[1243px]');
|
||||||
|
myCoolDiv.value.childNodes[0].classList.add('xl:h-[621.5px]');
|
||||||
|
myCoolDiv.value.childNodes[0].classList.add('lg:w-[1243px]');
|
||||||
|
myCoolDiv.value.childNodes[0].classList.add('lg:h-[621.5px]');
|
||||||
|
myCoolDiv.value.childNodes[0].classList.add('md:w-[900px]');
|
||||||
|
myCoolDiv.value.childNodes[0].classList.add('md:h-[400px]');
|
||||||
|
myCoolDiv.value.childNodes[0].classList.add('w-[310px]');
|
||||||
|
myCoolDiv.value.childNodes[0].classList.add('h-[250px]');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
372
inspond-nuxt-safekiso/base/components/BaseCommentCtl1.vue
Normal file
372
inspond-nuxt-safekiso/base/components/BaseCommentCtl1.vue
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Comments-->
|
||||||
|
<section aria-labelledby="notes-title">
|
||||||
|
<div
|
||||||
|
class="border border-gray-300 shadow sm:rounded-lg sm:overflow-hidden"
|
||||||
|
>
|
||||||
|
<div class="divide-y divide-gray-200">
|
||||||
|
<div class="px-4 py-5 sm:px-6">
|
||||||
|
<h2
|
||||||
|
id="notes-title"
|
||||||
|
class="text-lg font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-6 sm:px-6">
|
||||||
|
<ul role="list" class="space-y-8">
|
||||||
|
<li
|
||||||
|
v-for="commentItem in listData"
|
||||||
|
:key="commentItem.serial"
|
||||||
|
>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<div class="flex-shrink-0 items-center">
|
||||||
|
<BaseAvater1
|
||||||
|
:image-size="2"
|
||||||
|
:image-url="commentItem.profile_url"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<a
|
||||||
|
:href="
|
||||||
|
'/user/profile/' +
|
||||||
|
commentItem.pid
|
||||||
|
"
|
||||||
|
class="font-medium text-gray-900"
|
||||||
|
>{{ commentItem.nick }}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mt-1 text-sm text-gray-700 break-all"
|
||||||
|
>
|
||||||
|
<p>{{ commentItem.comment }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm space-x-2">
|
||||||
|
<span
|
||||||
|
class="text-gray-500 font-medium"
|
||||||
|
>{{
|
||||||
|
$dayjs(
|
||||||
|
commentItem.created
|
||||||
|
).fromNow()
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
{{ ' ' }}
|
||||||
|
<span
|
||||||
|
v-if="!readOnlyFlag"
|
||||||
|
class="text-gray-500 font-medium"
|
||||||
|
>·</span
|
||||||
|
>
|
||||||
|
{{ ' ' }}
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="commentItem.like_count != 0"
|
||||||
|
class="text-gray-500 font-medium"
|
||||||
|
>{{
|
||||||
|
'좋아요 ' +
|
||||||
|
_utils.formatNumberInBytesStyle(
|
||||||
|
commentItem.like_count,
|
||||||
|
0
|
||||||
|
) +
|
||||||
|
'개'
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
commentItem.dislike_count != 0
|
||||||
|
"
|
||||||
|
class="text-gray-500 font-medium"
|
||||||
|
>{{
|
||||||
|
'싫어요 ' +
|
||||||
|
_utils.formatNumberInBytesStyle(
|
||||||
|
commentItem.dislike_count,
|
||||||
|
0
|
||||||
|
) +
|
||||||
|
'개'
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="commentItem.report_count != 0"
|
||||||
|
class="text-gray-500 font-medium"
|
||||||
|
>{{
|
||||||
|
'신고 ' +
|
||||||
|
_utils.formatNumberInBytesStyle(
|
||||||
|
commentItem.report_count,
|
||||||
|
0
|
||||||
|
) +
|
||||||
|
'개'
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="
|
||||||
|
commentItem.myFlag == true &&
|
||||||
|
!readOnlyFlag
|
||||||
|
"
|
||||||
|
type="button"
|
||||||
|
class="text-gray-900 font-medium"
|
||||||
|
@click="
|
||||||
|
doAction(
|
||||||
|
'delete',
|
||||||
|
commentItem.cid
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!readOnlyFlag"
|
||||||
|
type="button"
|
||||||
|
class="text-gray-900 font-medium"
|
||||||
|
@click="
|
||||||
|
doAction(
|
||||||
|
'like',
|
||||||
|
commentItem.cid
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
좋아요
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!readOnlyFlag"
|
||||||
|
type="button"
|
||||||
|
class="text-gray-900 font-medium"
|
||||||
|
@click="
|
||||||
|
doAction(
|
||||||
|
'dislike',
|
||||||
|
commentItem.cid
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
싫어요
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="!readOnlyFlag"
|
||||||
|
type="button"
|
||||||
|
class="text-gray-900 font-medium"
|
||||||
|
@click="
|
||||||
|
doAction(
|
||||||
|
'report',
|
||||||
|
commentItem.cid
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
신고
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="!readOnlyFlag"
|
||||||
|
type="button"
|
||||||
|
class="text-gray-900 font-medium"
|
||||||
|
@click="
|
||||||
|
doAction(
|
||||||
|
'cancel',
|
||||||
|
commentItem.cid
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
신고 취소
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li v-if="listData.length == 0">
|
||||||
|
<div>등록된 댓글이 없습니다.</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<BasePagination1
|
||||||
|
class="mb-5 px-4 sm:px-6"
|
||||||
|
:total-page-count="totalPageCount"
|
||||||
|
:current-page-number="currentPageNumber"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:records-total="recordsTotal"
|
||||||
|
:page-move="pageMove"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="!readOnlyFlag" class="bg-gray-50 px-4 py-6 sm:px-6">
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<BaseUserProfileImage :image-size="2" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<form>
|
||||||
|
<div>
|
||||||
|
<label for="comment" class="sr-only">{{
|
||||||
|
title
|
||||||
|
}}</label>
|
||||||
|
<textarea
|
||||||
|
id="comment"
|
||||||
|
v-model="comment"
|
||||||
|
style="resize: none"
|
||||||
|
name="comment"
|
||||||
|
rows="3"
|
||||||
|
class="shadow-sm block w-full focus:ring-blue-500 focus:border-blue-500 sm:text-sm border border-gray-300 rounded-md"
|
||||||
|
placeholder="댓글 내용을 입력하세요."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex items-center justify-between">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="group inline-flex items-start text-sm space-x-2 text-gray-500 hover:text-gray-900"
|
||||||
|
>
|
||||||
|
<QuestionMarkCircleIcon
|
||||||
|
class="flex-shrink-0 h-5 w-5 text-gray-400 group-hover:text-gray-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
모욕적인 표현이 포함된 댓글은 등록이
|
||||||
|
거부될 수 있습니다.
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
@click="addComment()"
|
||||||
|
>
|
||||||
|
댓글 등록
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { QuestionMarkCircleIcon } from '@heroicons/vue/24/solid';
|
||||||
|
|
||||||
|
const comment = ref('');
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tid: { type: String, required: true },
|
||||||
|
title: { type: String, default: '댓글' },
|
||||||
|
readOnlyFlag: { type: Boolean, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log('huk props = ', props);
|
||||||
|
|
||||||
|
const title = ref(props.title);
|
||||||
|
|
||||||
|
console.log('title = ', title.value);
|
||||||
|
|
||||||
|
const listData = ref([]);
|
||||||
|
|
||||||
|
const readOnlyFlag = ref(props.readOnlyFlag);
|
||||||
|
|
||||||
|
const totalPageCount = ref(1);
|
||||||
|
const currentPageNumber = ref(Number.MAX_SAFE_INTEGER);
|
||||||
|
const pageSize = ref(3);
|
||||||
|
const recordsTotal = ref(0);
|
||||||
|
|
||||||
|
function pageMove(targetPageIdex) {
|
||||||
|
// console.log('on pageMove(), targetPageIdex=', targetPageIdex);
|
||||||
|
currentPageNumber.value = targetPageIdex;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addComment() {
|
||||||
|
if (comment.value.trim() != '') {
|
||||||
|
const responseJson = await _crossCtl.doComm('insert', 'comment', {
|
||||||
|
hero: props.tid,
|
||||||
|
comment: comment.value,
|
||||||
|
for: 'board',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
comment.value = '';
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
if (props.tid != '') {
|
||||||
|
const responseJson = await _crossCtl.doComm('list', 'comment:active', {
|
||||||
|
hero: props.tid,
|
||||||
|
start: (currentPageNumber.value - 1) * pageSize.value,
|
||||||
|
length: pageSize.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
currentPageNumber.value = responseJson['currentPageNumber'];
|
||||||
|
totalPageCount.value = responseJson['totalPageCount'];
|
||||||
|
pageSize.value = parseInt(responseJson['pageSize']);
|
||||||
|
recordsTotal.value = responseJson['recordsTotal'];
|
||||||
|
|
||||||
|
listData.value = responseJson['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.tid != '' && _crossCtl.isAuthenticated) {
|
||||||
|
const responseJson = await _crossCtl.doComm('list', 'like', {
|
||||||
|
hero: props.tid,
|
||||||
|
start: 0,
|
||||||
|
length: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
console.log('like responseJson=', responseJson);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doAction(tag, target) {
|
||||||
|
console.log('in doAction(), tag =', tag, ', target =', target);
|
||||||
|
let tmpResponseJson = null;
|
||||||
|
switch (tag) {
|
||||||
|
case 'like':
|
||||||
|
case 'dislike':
|
||||||
|
tmpResponseJson = await _crossCtl.doComm('update', 'like', {
|
||||||
|
domain: props.tid,
|
||||||
|
hero: target,
|
||||||
|
for: 'comment',
|
||||||
|
tag: tag,
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
break;
|
||||||
|
case 'report':
|
||||||
|
tmpResponseJson = await _crossCtl.doComm('update', 'report', {
|
||||||
|
domain: props.tid,
|
||||||
|
hero: target,
|
||||||
|
for: 'comment',
|
||||||
|
tag: tag,
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
tmpResponseJson = await _crossCtl.doComm('update', 'report', {
|
||||||
|
domain: props.tid,
|
||||||
|
hero: target,
|
||||||
|
for: 'comment',
|
||||||
|
tag: tag,
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
tmpResponseJson = await _crossCtl.doComm('delete', 'comment', {
|
||||||
|
hero: target,
|
||||||
|
tid: props.tid,
|
||||||
|
from: 'board',
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
25
inspond-nuxt-safekiso/base/components/BaseFaqItem1.vue
Normal file
25
inspond-nuxt-safekiso/base/components/BaseFaqItem1.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<dt class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ item.question }}
|
||||||
|
</dt>
|
||||||
|
|
||||||
|
<dd
|
||||||
|
class="mt-2 text-base text-gray-500"
|
||||||
|
style="
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-family: inherit;
|
||||||
|
"
|
||||||
|
v-html="item.answer.replace(/(?:\r\n|\r|\n)/g, '<br />')"
|
||||||
|
></dd>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
item: { type: Object, required: true },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
||||||
141
inspond-nuxt-safekiso/base/components/BaseList1.vue
Normal file
141
inspond-nuxt-safekiso/base/components/BaseList1.vue
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pb-8 px-0 sm:px-0 lg:px-0">
|
||||||
|
<div
|
||||||
|
class="-mx-4 mt-8 overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:-mx-6 md:mx-0 md:rounded-lg"
|
||||||
|
>
|
||||||
|
<table class="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="(heading, index) in headings"
|
||||||
|
:key="index"
|
||||||
|
scope="col"
|
||||||
|
:class="heading['class']"
|
||||||
|
>
|
||||||
|
{{ heading['title'] }}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index + headings.length"
|
||||||
|
scope="col"
|
||||||
|
class="relative py-3.5 pl-3 pr-4 sm:pr-6"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ action }}</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 bg-white">
|
||||||
|
<tr v-if="data.length == 0">
|
||||||
|
<td>
|
||||||
|
<div class="py-3.5 pl-3 pr-4 sm:pr-6">
|
||||||
|
{{ noDataMessage }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="(item, itemIndex) in data" :key="itemIndex">
|
||||||
|
<td
|
||||||
|
v-for="(heading, index) in headings"
|
||||||
|
:key="index"
|
||||||
|
:class="heading['subClass']"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
columnFilter
|
||||||
|
? columnFilter(
|
||||||
|
heading['key'],
|
||||||
|
item[heading['key']]
|
||||||
|
)
|
||||||
|
: item[heading['key']]
|
||||||
|
}}
|
||||||
|
|
||||||
|
<dl
|
||||||
|
v-for="(hItem, hItemIndex) in heading[
|
||||||
|
'hiddenInfo'
|
||||||
|
]['dts']"
|
||||||
|
:key="hItemIndex"
|
||||||
|
:class="heading['hiddenInfo']['headClass']"
|
||||||
|
>
|
||||||
|
<dt
|
||||||
|
:class="
|
||||||
|
heading['hiddenInfo']['dts'][
|
||||||
|
hItemIndex
|
||||||
|
]['class']
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
heading['hiddenInfo']['dts'][
|
||||||
|
hItemIndex
|
||||||
|
]['title']
|
||||||
|
}}
|
||||||
|
</dt>
|
||||||
|
<dd
|
||||||
|
:class="
|
||||||
|
heading['hiddenInfo']['dds'][
|
||||||
|
hItemIndex
|
||||||
|
]['class']
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
columnFilter
|
||||||
|
? columnFilter(
|
||||||
|
heading['hiddenInfo']['dds'][
|
||||||
|
hItemIndex
|
||||||
|
]['key'],
|
||||||
|
item[
|
||||||
|
heading['hiddenInfo'][
|
||||||
|
'dds'
|
||||||
|
][hItemIndex]['key']
|
||||||
|
]
|
||||||
|
)
|
||||||
|
: item[
|
||||||
|
heading['hiddenInfo']['dds'][
|
||||||
|
hItemIndex
|
||||||
|
]['key']
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td
|
||||||
|
class="py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="text-indigo-600 hover:text-indigo-900"
|
||||||
|
@click="doAction(action, item[actionKey])"
|
||||||
|
>{{ action
|
||||||
|
}}<span class="sr-only"
|
||||||
|
>, {{ item['serial'] }}</span
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
headings: { type: Array<object>, required: true },
|
||||||
|
actions: { type: Array<string>, default: [] },
|
||||||
|
keys: { type: Array<string>, default: [] },
|
||||||
|
data: { type: Array<object>, required: true },
|
||||||
|
actionKey: { type: String, default: 'serial' },
|
||||||
|
columnFilter: {
|
||||||
|
type: Function,
|
||||||
|
default: (key: string, val: string) => val,
|
||||||
|
},
|
||||||
|
doAction: {
|
||||||
|
type: Function,
|
||||||
|
default: (key: string) => {
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
noDataMessage: { type: String, default: '검색된 데이터가 없습니다.' },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
125
inspond-nuxt-safekiso/base/components/BaseMainDataList2.vue
Normal file
125
inspond-nuxt-safekiso/base/components/BaseMainDataList2.vue
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col overflow-hidden rounded-lg shadow-lg">
|
||||||
|
<div
|
||||||
|
v-if="isLoading"
|
||||||
|
class="flex flex-1 flex-col justify-between bg-white p-6"
|
||||||
|
>
|
||||||
|
<div class="flex-1 relative">
|
||||||
|
<div class="flex border-b-2 border-indigo-500 pb-2">
|
||||||
|
<p class="text-sm font-medium text-indigo-600">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
@click="pageMove(pageTitle[0].id, '')"
|
||||||
|
>{{
|
||||||
|
pageTitle[0]?.title ?? pageTitle[0]?.title
|
||||||
|
}} </a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-6 h-6 text-indigo-500 absolute right-0 cursor-pointer"
|
||||||
|
@click="pageMove(pageTitle[0].id, '')"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 6v12m6-6H6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
v-for="list in dataList"
|
||||||
|
:key="list.serial"
|
||||||
|
class="mt-2 block"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="mt-3 text-base text-gray-500 cursor-pointer"
|
||||||
|
@click="pageMove(pageTitle[0].id, list)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
list.title.length >= 20
|
||||||
|
? list.title.substr(0, 20) + '...'
|
||||||
|
: list.title.substr(0, 20)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-1 flex-col justify-between bg-white p-6">
|
||||||
|
<div class="flex-1 relative">
|
||||||
|
<div class="flex pb-2 bg-sky-100 animate-pulse rounded-lg">
|
||||||
|
<p class="text-sm font-medium text-gray-600">
|
||||||
|
<a href="javascript:void(0)"> </a>
|
||||||
|
</p>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="hidden w-6 h-6 text-indigo-500 absolute right-0 cursor-pointer"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 6v12m6-6H6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<a class="mt-2 block">
|
||||||
|
<p
|
||||||
|
class="mt-3 bg-slate-100 animate-pulse rounded-lg cursor-pointer w-3/4"
|
||||||
|
>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="mt-3 bg-slate-100 animate-pulse rounded-lg cursor-pointer w-full"
|
||||||
|
>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="mt-3 bg-slate-100 animate-pulse rounded-lg cursor-pointer w-2/3"
|
||||||
|
>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="mt-3 bg-slate-100 animate-pulse rounded-lg cursor-pointer w-3/4"
|
||||||
|
>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="mt-3 bg-slate-100 animate-pulse rounded-lg cursor-pointer w-3/4"
|
||||||
|
>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
dataList: { type: Object, required: true },
|
||||||
|
pageTitle: { type: Object, required: true },
|
||||||
|
isLoading: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const bordPage = ['preach', 'programme', 'youtube'];
|
||||||
|
|
||||||
|
function pageMove(pageName, detail) {
|
||||||
|
if (pageName === 'notice') {
|
||||||
|
navigateTo('/support/' + pageName);
|
||||||
|
} else if (bordPage.includes(pageName) == true) {
|
||||||
|
if (detail == '') {
|
||||||
|
navigateTo('/board/' + pageName + '/list');
|
||||||
|
} else {
|
||||||
|
navigateTo('/board/' + pageName + '/view/' + detail.cid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
131
inspond-nuxt-safekiso/base/components/BaseModal1.vue
Normal file
131
inspond-nuxt-safekiso/base/components/BaseModal1.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<TransitionRoot as="template" :show="open">
|
||||||
|
<Dialog as="div" class="relative z-10" @close="open = false">
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="fixed z-10 inset-0 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0"
|
||||||
|
>
|
||||||
|
<DialogPanel
|
||||||
|
class="relative bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full"
|
||||||
|
>
|
||||||
|
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div class="sm:flex sm:items-start">
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
currentModalInfo['type'] == 'ok'
|
||||||
|
? 'mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10'
|
||||||
|
: currentModalInfo['type'] ==
|
||||||
|
'error'
|
||||||
|
? 'mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10'
|
||||||
|
: 'mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-gray-100 sm:mx-0 sm:h-10 sm:w-10'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
v-if="currentModalInfo['type'] == 'ok'"
|
||||||
|
class="h-6 w-6 text-green-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<ExclamationTriangleIcon
|
||||||
|
v-else-if="
|
||||||
|
currentModalInfo['type'] == 'error'
|
||||||
|
"
|
||||||
|
class="h-6 w-6 text-red-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<ExclamationCircleIcon
|
||||||
|
v-else
|
||||||
|
class="h-6 w-6 text-gray-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"
|
||||||
|
>
|
||||||
|
<DialogTitle
|
||||||
|
as="h3"
|
||||||
|
class="text-lg leading-6 font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{ currentModalInfo['title'] }}
|
||||||
|
</DialogTitle>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{{ currentModalInfo['message'] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="
|
||||||
|
currentModalInfo['type'] == 'ok'
|
||||||
|
? 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm'
|
||||||
|
: currentModalInfo['type'] == 'error'
|
||||||
|
? 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm'
|
||||||
|
: 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-600 text-base font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:ml-3 sm:w-auto sm:text-sm'
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
_crossCtl.onModalClosed(
|
||||||
|
currentModalInfo.serial,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ currentModalInfo['btnTexts'][0] }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="currentModalInfo['btnCount'] > 1"
|
||||||
|
ref="cancelButtonRef"
|
||||||
|
type="button"
|
||||||
|
:class="
|
||||||
|
currentModalInfo['type'] == 'ok'
|
||||||
|
? 'mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm'
|
||||||
|
: currentModalInfo['type'] == 'error'
|
||||||
|
? 'mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm'
|
||||||
|
: 'mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm'
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
_crossCtl.onModalClosed(
|
||||||
|
currentModalInfo.serial,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ currentModalInfo['btnTexts'][1] }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
DialogTitle,
|
||||||
|
TransitionRoot,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
} from '@heroicons/vue/24/outline';
|
||||||
|
|
||||||
|
const currentModalInfo = _crossCtl.currentModalInfo;
|
||||||
|
|
||||||
|
const open = computed(() => {
|
||||||
|
return currentModalInfo.value['serial'] !== -1;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
222
inspond-nuxt-safekiso/base/components/BaseNavSideBar1.vue
Normal file
222
inspond-nuxt-safekiso/base/components/BaseNavSideBar1.vue
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<div class="flex-1 h-0 pt-5 pb-4 overflow-y-auto bg-white">
|
||||||
|
<div class="flex-shrink-0 flex items-center px-4">
|
||||||
|
<a href="javascript:void(0)" @click="router.push('/')">
|
||||||
|
<!--{{ siteName }}-->
|
||||||
|
<img
|
||||||
|
src="/kiso_ci_1.png"
|
||||||
|
alt="KISO CI"
|
||||||
|
width="500"
|
||||||
|
height="600"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<nav class="flex-1 mt-5 px-2 space-y-1 bg-white" aria-label="Sidebar">
|
||||||
|
<template v-for="item in currentMenu['main']" :key="item.idx">
|
||||||
|
<div v-if="!item.subs">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="[
|
||||||
|
item['current']
|
||||||
|
? 'bg-gray-100 text-gray-900'
|
||||||
|
: 'bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900',
|
||||||
|
'group w-full flex items-center pl-2 py-2 text-sm font-medium rounded-md',
|
||||||
|
]"
|
||||||
|
@click="handleItemClick(item)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="item['icon']"
|
||||||
|
:class="[
|
||||||
|
item['current']
|
||||||
|
? 'text-gray-500'
|
||||||
|
: 'text-gray-400 group-hover:text-gray-500',
|
||||||
|
'mr-3 flex-shrink-0 h-6 w-6',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{{ item['title'] }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<Disclosure
|
||||||
|
v-else
|
||||||
|
v-slot="{ open, close }"
|
||||||
|
as="div"
|
||||||
|
class="space-y-1"
|
||||||
|
>
|
||||||
|
<DisclosureButton
|
||||||
|
:class="[
|
||||||
|
item['current']
|
||||||
|
? 'bg-gray-100 text-gray-900'
|
||||||
|
: 'bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900',
|
||||||
|
'group w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="item['icon']"
|
||||||
|
class="mr-3 flex-shrink-0 h-6 w-6 text-gray-400 group-hover:text-gray-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span class="flex-1">
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
:class="[
|
||||||
|
open
|
||||||
|
? 'text-gray-400 rotate-90'
|
||||||
|
: 'text-gray-300',
|
||||||
|
'ml-3 flex-shrink-0 h-5 w-5 transform group-hover:text-gray-400 transition-colors ease-in-out duration-150',
|
||||||
|
]"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M6 6L14 10L6 14V6Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</DisclosureButton>
|
||||||
|
<DisclosurePanel class="space-y-1">
|
||||||
|
<a
|
||||||
|
v-for="subItem in item.subs"
|
||||||
|
:key="subItem['idx']"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="[
|
||||||
|
subItem['current']
|
||||||
|
? 'bg-gray-100 text-gray-900'
|
||||||
|
: 'bg-white text-gray-600 hover:text-gray-900 hover:bg-gray-50',
|
||||||
|
'group w-full flex items-center pl-10 pr-2 py-2 text-sm font-medium rounded-md ',
|
||||||
|
]"
|
||||||
|
@click="handleSubItemClick(subItem)"
|
||||||
|
>
|
||||||
|
{{ subItem['title'] }}
|
||||||
|
</a>
|
||||||
|
</DisclosurePanel>
|
||||||
|
<button
|
||||||
|
v-show="false"
|
||||||
|
:ref="(el) => (elements[item.idx] = el)"
|
||||||
|
:data-id="item.idx"
|
||||||
|
@click="doClose(close)"
|
||||||
|
></button>
|
||||||
|
<DisclosureStateEmitter
|
||||||
|
:show="open"
|
||||||
|
@show="hideOther(item.idx)"
|
||||||
|
/>
|
||||||
|
</Disclosure>
|
||||||
|
</template>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-shrink-0 flex bg-white p-4">
|
||||||
|
<a href="#" class="flex-shrink-0 group block">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3
|
||||||
|
id="projects-headline"
|
||||||
|
class="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider"
|
||||||
|
></h3>
|
||||||
|
<div
|
||||||
|
class="space-y-1"
|
||||||
|
role="group"
|
||||||
|
aria-labelledby="projects-headline"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="group flex items-center px-3 py-2 text-sm font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50"
|
||||||
|
@click="doSignInAndOut()"
|
||||||
|
>
|
||||||
|
<span class="truncate">
|
||||||
|
{{ isAuthenticated ? '로그아웃' : '로그인' }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-for="item in currentMenu['sub']"
|
||||||
|
:key="item.idx"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="group flex items-center px-3 py-2 text-sm font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50"
|
||||||
|
@click="handleItemClick(item)"
|
||||||
|
>
|
||||||
|
<span class="truncate">
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
|
||||||
|
const props = defineProps({
|
||||||
|
onMove: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAuthenticated = _crossCtl.isAuthenticated;
|
||||||
|
|
||||||
|
const siteName = ref(_siteConfig.siteName);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const currentMenu = _crossCtl.menu;
|
||||||
|
|
||||||
|
const elements = ref([]);
|
||||||
|
|
||||||
|
async function doSignInAndOut() {
|
||||||
|
if (_crossCtl.isAuthenticated.value) {
|
||||||
|
const response = await _crossCtl.doComm('signout', '', {});
|
||||||
|
console.log('response=', response);
|
||||||
|
if (response['responseCode'] == 200) {
|
||||||
|
_crossCtl.setUserInfo({
|
||||||
|
isAdmin: false,
|
||||||
|
isApproved: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isHighLeveled: false,
|
||||||
|
isOp: false,
|
||||||
|
isSuperOp: false,
|
||||||
|
});
|
||||||
|
_crossCtl.setUserProfile({});
|
||||||
|
isAuthenticated.value = false;
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
alert(response['responseMessage']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("go user login");
|
||||||
|
navigateTo({
|
||||||
|
path: '/user/signin',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleItemClick(item) {
|
||||||
|
hideOther(Number.MAX_SAFE_INTEGER);
|
||||||
|
_crossCtl.moveToMenuItem(item);
|
||||||
|
props.onMove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubItemClick(item) {
|
||||||
|
_crossCtl.moveToMenuItem(item);
|
||||||
|
props.onMove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOther(id) {
|
||||||
|
let targetEl = null;
|
||||||
|
const items = elements.value.filter((elm) => {
|
||||||
|
if (elm.getAttribute('data-id') == id.toString()) {
|
||||||
|
targetEl = elm;
|
||||||
|
}
|
||||||
|
return elm.getAttribute('data-id') !== id.toString();
|
||||||
|
// return true;
|
||||||
|
});
|
||||||
|
items.forEach((elm) => elm.click());
|
||||||
|
|
||||||
|
// targetEl.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function doClose(close) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
31
inspond-nuxt-safekiso/base/components/BaseNoticeItem1.vue
Normal file
31
inspond-nuxt-safekiso/base/components/BaseNoticeItem1.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-w-0 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800 border-2 m-5"
|
||||||
|
>
|
||||||
|
<div class="ml-4">
|
||||||
|
<dt class="mt-3 text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ item.title }}
|
||||||
|
</dt>
|
||||||
|
<dd
|
||||||
|
class="mt-9 text-base text-gray-500"
|
||||||
|
v-html="
|
||||||
|
_utils
|
||||||
|
.escapeHtml(item.detail)
|
||||||
|
.replace(/(?:\r\n|\r|\n)/g, '<br />')
|
||||||
|
"
|
||||||
|
></dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-right text-xs">
|
||||||
|
{{ $customFormat(item.created) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
item: { type: Object, required: true },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
||||||
112
inspond-nuxt-safekiso/base/components/BasePageFooter1.vue
Normal file
112
inspond-nuxt-safekiso/base/components/BasePageFooter1.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<footer
|
||||||
|
class="footer px-10 py-4 bg-base-200 text-base-content border-base-300"
|
||||||
|
>
|
||||||
|
<div class="items-center grid-flow-col">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
class="fill-current"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M22.672 15.226l-2.432.811.841 2.515c.33 1.019-.209 2.127-1.23 2.456-1.15.325-2.148-.321-2.463-1.226l-.84-2.518-5.013 1.677.84 2.517c.391 1.203-.434 2.542-1.831 2.542-.88 0-1.601-.564-1.86-1.314l-.842-2.516-2.431.809c-1.135.328-2.145-.317-2.463-1.229-.329-1.018.211-2.127 1.231-2.456l2.432-.809-1.621-4.823-2.432.808c-1.355.384-2.558-.59-2.558-1.839 0-.817.509-1.582 1.327-1.846l2.433-.809-.842-2.515c-.33-1.02.211-2.129 1.232-2.458 1.02-.329 2.13.209 2.461 1.229l.842 2.515 5.011-1.677-.839-2.517c-.403-1.238.484-2.553 1.843-2.553.819 0 1.585.509 1.85 1.326l.841 2.517 2.431-.81c1.02-.33 2.131.211 2.461 1.229.332 1.018-.21 2.126-1.23 2.456l-2.433.809 1.622 4.823 2.433-.809c1.242-.401 2.557.484 2.557 1.838 0 .819-.51 1.583-1.328 1.847m-8.992-6.428l-5.01 1.675 1.619 4.828 5.011-1.674-1.62-4.829z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<p>{{ copyrightName }} <br />{{ siteSlogan }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="md:place-self-center md:justify-self-end">
|
||||||
|
<div class="grid grid-flow-col gap-4">
|
||||||
|
<a v-for="item in snsLinks" :key="item.tag" :href="item.url">
|
||||||
|
<component
|
||||||
|
:is="socialLogs[item.tag].icon"
|
||||||
|
v-if="socialLogs[item.tag] != undefined"
|
||||||
|
class="h-6 w-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span v-else class="">{{ item.tag }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const copyrightName = ref(_siteConfig.copyrightName);
|
||||||
|
const siteSlogan = ref(_siteConfig.siteSlogan);
|
||||||
|
const snsLinks = ref(_siteConfig.snsLinks);
|
||||||
|
|
||||||
|
const socialLogs = {
|
||||||
|
facebook: {
|
||||||
|
name: 'Facebook',
|
||||||
|
href: '#',
|
||||||
|
icon: defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
|
||||||
|
h('path', {
|
||||||
|
'fill-rule': 'evenodd',
|
||||||
|
d: 'M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z',
|
||||||
|
'clip-rule': 'evenodd',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
instagram: {
|
||||||
|
name: 'Instagram',
|
||||||
|
href: '#',
|
||||||
|
icon: defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
|
||||||
|
h('path', {
|
||||||
|
'fill-rule': 'evenodd',
|
||||||
|
d: 'M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z',
|
||||||
|
'clip-rule': 'evenodd',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
name: 'Twitter',
|
||||||
|
href: '#',
|
||||||
|
icon: defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
|
||||||
|
h('path', {
|
||||||
|
d: 'M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
gitHub: {
|
||||||
|
name: 'GitHub',
|
||||||
|
href: '#',
|
||||||
|
icon: defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
|
||||||
|
h('path', {
|
||||||
|
'fill-rule': 'evenodd',
|
||||||
|
d: 'M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z',
|
||||||
|
'clip-rule': 'evenodd',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
dribbble: {
|
||||||
|
name: 'Dribbble',
|
||||||
|
href: '#',
|
||||||
|
icon: defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
|
||||||
|
h('path', {
|
||||||
|
'fill-rule': 'evenodd',
|
||||||
|
d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2zm6.605 4.61a8.502 8.502 0 011.93 5.314c-.281-.054-3.101-.629-5.943-.271-.065-.141-.12-.293-.184-.445a25.416 25.416 0 00-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362zM12 3.475c2.17 0 4.154.813 5.662 2.148-.152.216-1.443 1.941-4.48 3.08-1.399-2.57-2.95-4.675-3.189-5A8.687 8.687 0 0112 3.475zm-3.633.803a53.896 53.896 0 013.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.581 8.581 0 014.729-5.975zM3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215.25.477.477.965.694 1.453-.109.033-.228.065-.336.098-4.404 1.42-6.747 5.303-6.942 5.629a8.522 8.522 0 01-2.19-5.705zM12 20.547a8.482 8.482 0 01-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337.022-.01.033-.01.054-.022a35.318 35.318 0 011.823 6.475 8.4 8.4 0 01-3.341.684zm4.761-1.465c-.086-.52-.542-3.015-1.659-6.084 2.679-.423 5.022.271 5.314.369a8.468 8.468 0 01-3.655 5.715z',
|
||||||
|
'clip-rule': 'evenodd',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
289
inspond-nuxt-safekiso/base/components/BasePagination1.vue
Normal file
289
inspond-nuxt-safekiso/base/components/BasePagination1.vue
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="pt-5 px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6"
|
||||||
|
>
|
||||||
|
<div class="flex-1 flex justify-between sm:hidden">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:text-gray-500"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:text-gray-500"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-700">
|
||||||
|
Showing
|
||||||
|
<span class="font-medium">{{ showingFrom }}</span>
|
||||||
|
to
|
||||||
|
<span class="font-medium">{{ showingTo }}</span>
|
||||||
|
of
|
||||||
|
<span class="font-medium">{{ recordsTotal }}</span>
|
||||||
|
results
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<nav
|
||||||
|
class="relative z-0 inline-flex shadow-sm -space-x-px"
|
||||||
|
aria-label="Pagination"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
||||||
|
@click="gotoPage('first', 0)"
|
||||||
|
>
|
||||||
|
<span class="sr-only">First</span>
|
||||||
|
<!-- Heroicon name: chevron-double-left -->
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
||||||
|
@click="gotoPage('prev', 0)"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Previous</span>
|
||||||
|
<!-- Heroicon name: chevron-left -->
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-for="(item, index) in currentSlots"
|
||||||
|
:key="index"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="
|
||||||
|
currentSlots[index] == currentPageNumber
|
||||||
|
? 'bg-gray-200'
|
||||||
|
: 'bg-white'
|
||||||
|
"
|
||||||
|
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
|
@click="gotoPage(currentSlots[index], index)"
|
||||||
|
>
|
||||||
|
{{ currentSlots[index] }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
||||||
|
@click="gotoPage('next', 0)"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Next</span>
|
||||||
|
<!-- Heroicon name: chevron-right -->
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
||||||
|
@click="gotoPage('last', 0)"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Last</span>
|
||||||
|
<!-- Heroicon name: chevron-double-right -->
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 5l7 7-7 7M5 5l7 7-7 7"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from '@heroicons/vue/solid/index.js';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
totalPageCount: { type: Number, default: 1 },
|
||||||
|
currentPageNumber: { type: Number, default: 1 },
|
||||||
|
pageSize: { type: Number, default: 10 },
|
||||||
|
recordsTotal: { type: Number, default: 0 },
|
||||||
|
pageMove: { type: Function, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const pagenationSize = 5;
|
||||||
|
|
||||||
|
//console.log(props);
|
||||||
|
|
||||||
|
const showingFrom = computed(() => {
|
||||||
|
const result = 1 + (props.currentPageNumber - 1) * props.pageSize;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
//console.log(showingFrom.value);
|
||||||
|
|
||||||
|
const showingTo = computed(() => {
|
||||||
|
let result = props.currentPageNumber * props.pageSize;
|
||||||
|
if (result > props.recordsTotal) {
|
||||||
|
result = props.recordsTotal;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentSlots = computed(() => {
|
||||||
|
const result: string[] = [];
|
||||||
|
if (props.totalPageCount > pagenationSize) {
|
||||||
|
if (props.currentPageNumber < Math.ceil(pagenationSize / 2)) {
|
||||||
|
for (let i = 1; i < pagenationSize; i++) {
|
||||||
|
// good for 3, 4, ....
|
||||||
|
result.push(i.toString());
|
||||||
|
}
|
||||||
|
result.push('...');
|
||||||
|
} else if (
|
||||||
|
props.currentPageNumber >
|
||||||
|
props.totalPageCount - pagenationSize / 2
|
||||||
|
) {
|
||||||
|
result.push('...');
|
||||||
|
for (
|
||||||
|
let i = props.totalPageCount - pagenationSize + 1;
|
||||||
|
i <= props.totalPageCount;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
// good for 3, 4, ....
|
||||||
|
result.push(i.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push('...');
|
||||||
|
for (
|
||||||
|
let i =
|
||||||
|
props.currentPageNumber - Math.floor(pagenationSize / 2);
|
||||||
|
i < props.currentPageNumber + Math.ceil(pagenationSize / 2);
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
// good for 3, 4, ....
|
||||||
|
result.push(i.toString());
|
||||||
|
}
|
||||||
|
result.push('...');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 1; i <= props.totalPageCount; i++) {
|
||||||
|
result.push(i.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('result = ', result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
function gotoPage(target, opt) {
|
||||||
|
//console.log('gotoPage, target=', target);
|
||||||
|
let targetPageIndex = props.currentPageNumber;
|
||||||
|
switch (target) {
|
||||||
|
case 'first':
|
||||||
|
targetPageIndex = 1;
|
||||||
|
break;
|
||||||
|
case 'prev':
|
||||||
|
if (props.currentPageNumber > 1) {
|
||||||
|
targetPageIndex = props.currentPageNumber - 1;
|
||||||
|
} else {
|
||||||
|
targetPageIndex = props.currentPageNumber;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'next':
|
||||||
|
if (props.totalPageCount > props.currentPageNumber) {
|
||||||
|
targetPageIndex = props.currentPageNumber + 1;
|
||||||
|
} else {
|
||||||
|
targetPageIndex = props.currentPageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'last':
|
||||||
|
targetPageIndex = props.totalPageCount;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '...':
|
||||||
|
if (opt == 0) {
|
||||||
|
if (props.currentPageNumber - pagenationSize > 0) {
|
||||||
|
targetPageIndex = props.currentPageNumber - pagenationSize;
|
||||||
|
} else {
|
||||||
|
targetPageIndex = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
props.currentPageNumber + pagenationSize <
|
||||||
|
props.totalPageCount
|
||||||
|
) {
|
||||||
|
targetPageIndex = props.currentPageNumber + pagenationSize;
|
||||||
|
} else {
|
||||||
|
targetPageIndex = props.totalPageCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
const tmpPageIdx = parseInt(target);
|
||||||
|
if (tmpPageIdx >= 1 && tmpPageIdx <= props.totalPageCount) {
|
||||||
|
targetPageIndex = tmpPageIdx;
|
||||||
|
} else {
|
||||||
|
targetPageIndex = props.currentPageNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//console.log('final targetPageIdex = ', targetPageIndex);
|
||||||
|
|
||||||
|
// console.log('huk', this);
|
||||||
|
// this.$parent.pageMove(targetPageIndex);
|
||||||
|
// $emit('pageMove', targetPageIndex);
|
||||||
|
props.pageMove(targetPageIndex);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
85
inspond-nuxt-safekiso/base/components/BaseTable1.vue
Normal file
85
inspond-nuxt-safekiso/base/components/BaseTable1.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="">
|
||||||
|
<div class="card border overflow-x-auto">
|
||||||
|
<table class="table w-full">
|
||||||
|
<!-- head -->
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="(heading, index) in headings"
|
||||||
|
:key="index"
|
||||||
|
:width="
|
||||||
|
heading['widthRatio'] != ''
|
||||||
|
? heading['widthRatio'] + '%'
|
||||||
|
: '100%'
|
||||||
|
"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
{{ heading['title'] }}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index + headings.length"
|
||||||
|
:width="actions.length * 1"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ action }}</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(item, itemIndex) in data" :key="itemIndex">
|
||||||
|
<td v-for="(heading, index) in headings" :key="index">
|
||||||
|
{{
|
||||||
|
columnFilter
|
||||||
|
? columnFilter(
|
||||||
|
heading['key'],
|
||||||
|
item[heading['key']]
|
||||||
|
)
|
||||||
|
: item[heading['key']]
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index"
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
:class="index != 0 ? 'ml-1' : ''"
|
||||||
|
@click="doAction(action, item[actionKey])"
|
||||||
|
>
|
||||||
|
{{ action
|
||||||
|
}}<span class="sr-only"
|
||||||
|
>, {{ item['serial'] }}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="data.length == 0">
|
||||||
|
<td>{{ 'No Data to display...' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
headings: { type: Array<object>, required: true },
|
||||||
|
actions: { type: Array<string>, default: [] },
|
||||||
|
data: { type: Array<object>, required: true },
|
||||||
|
actionKey: { type: String, default: 'serial' },
|
||||||
|
columnFilter: {
|
||||||
|
type: Function,
|
||||||
|
default: (key: string, val: string) => val,
|
||||||
|
},
|
||||||
|
doAction: {
|
||||||
|
type: Function,
|
||||||
|
default: (key: string) => {
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
110
inspond-nuxt-safekiso/base/components/BaseTable2.vue
Normal file
110
inspond-nuxt-safekiso/base/components/BaseTable2.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div
|
||||||
|
class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<table class="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="(heading, index) in headings"
|
||||||
|
:key="index"
|
||||||
|
:width="
|
||||||
|
heading['widthRatio'] != ''
|
||||||
|
? heading['widthRatio'] + '%'
|
||||||
|
: '100%'
|
||||||
|
"
|
||||||
|
:class="
|
||||||
|
index == 0
|
||||||
|
? 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 md:pl-0'
|
||||||
|
: 'py-3.5 px-3 text-left text-sm font-semibold text-gray-900'
|
||||||
|
"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
{{ heading['title'] }}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index + headings.length"
|
||||||
|
:width="actions.length * 1"
|
||||||
|
class="relative py-3.5 pl-3 pr-4 sm:pr-6 md:pr-0"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ action }}</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
<tr v-for="(item, itemIndex) in data" :key="itemIndex">
|
||||||
|
<td
|
||||||
|
v-for="(heading, index) in headings"
|
||||||
|
:key="index"
|
||||||
|
:class="
|
||||||
|
index == 0
|
||||||
|
? 'whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 md:pl-0'
|
||||||
|
: 'whitespace-nowrap py-4 px-3 text-sm text-gray-500'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
columnFilter
|
||||||
|
? columnFilter(
|
||||||
|
heading['key'],
|
||||||
|
item[heading['key']]
|
||||||
|
)
|
||||||
|
: item[heading['key']]
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td
|
||||||
|
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 md:pr-0"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="index != 0 ? 'ml-3' : ''"
|
||||||
|
class="text-indigo-600 hover:text-indigo-900"
|
||||||
|
@click="doAction(action, item[actionKey])"
|
||||||
|
>{{ action
|
||||||
|
}}<span class="sr-only"
|
||||||
|
>, {{ item['serial'] }}</span
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="data.length == 0">
|
||||||
|
<td
|
||||||
|
:colspan="headings.length"
|
||||||
|
class="whitespace-nowrap py-4 px-3 text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
{{ noDataMessage }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
headings: { type: Array<object>, required: true },
|
||||||
|
actions: { type: Array<string>, default: [] },
|
||||||
|
data: { type: Array<object>, required: true },
|
||||||
|
actionKey: { type: String, default: 'serial' },
|
||||||
|
columnFilter: {
|
||||||
|
type: Function,
|
||||||
|
default: (key: string, val: string) => val,
|
||||||
|
},
|
||||||
|
doAction: {
|
||||||
|
type: Function,
|
||||||
|
default: (key: string) => {
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
noDataMessage: { type: String, default: '조회된 데이터가 없습니다.' },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<BaseAvater1
|
||||||
|
:image-size="photoSize"
|
||||||
|
:image-url="photoUrl"
|
||||||
|
:image-alt="photoAlt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
imageSize: { type: Number, default: 12 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const photoSize = ref(props.imageSize);
|
||||||
|
const photoUrl = _crossCtl.profileUrlRef;
|
||||||
|
const photoAlt = ref(
|
||||||
|
_crossCtl.isAuthenticated
|
||||||
|
? _crossCtl.userInfo['userInfo']['userName'] + '의 프로필 사진'
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div v-show="false"></div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['show', 'hide'],
|
||||||
|
watch: {
|
||||||
|
show(show) {
|
||||||
|
if (show) {
|
||||||
|
this.$emit('show');
|
||||||
|
} else {
|
||||||
|
this.$emit('hide');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
106
inspond-nuxt-safekiso/base/components/Footer1.vue
Normal file
106
inspond-nuxt-safekiso/base/components/Footer1.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<footer class="bg-white">
|
||||||
|
<div
|
||||||
|
class="mx-auto max-w-7xl py-12 px-4 sm:px-6 md:flex md:items-center md:justify-between lg:px-8"
|
||||||
|
>
|
||||||
|
<div class="flex justify-center space-x-6 md:order-2">
|
||||||
|
<a
|
||||||
|
v-for="item in snsLinks"
|
||||||
|
:key="item.tag"
|
||||||
|
:href="item.url"
|
||||||
|
class="text-gray-400 hover:text-gray-500"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ item.tag }}</span>
|
||||||
|
<component
|
||||||
|
:is="socialLogos[item.tag].icon"
|
||||||
|
v-if="socialLogos[item.tag] != undefined"
|
||||||
|
class="h-6 w-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 md:order-1 md:mt-0">
|
||||||
|
<p class="text-center text-base text-gray-400">
|
||||||
|
{{ copyrightName }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const copyrightName = ref(_siteConfig.copyrightName);
|
||||||
|
const snsLinks = ref(_siteConfig.snsLinks);
|
||||||
|
|
||||||
|
const socialLogos = {
|
||||||
|
facebook: {
|
||||||
|
name: 'Facebook',
|
||||||
|
href: '#',
|
||||||
|
icon: defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
|
||||||
|
h('path', {
|
||||||
|
'fill-rule': 'evenodd',
|
||||||
|
d: 'M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z',
|
||||||
|
'clip-rule': 'evenodd',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
instagram: {
|
||||||
|
name: 'Instagram',
|
||||||
|
href: '#',
|
||||||
|
icon: defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
|
||||||
|
h('path', {
|
||||||
|
'fill-rule': 'evenodd',
|
||||||
|
d: 'M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z',
|
||||||
|
'clip-rule': 'evenodd',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
name: 'Twitter',
|
||||||
|
href: '#',
|
||||||
|
icon: defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
|
||||||
|
h('path', {
|
||||||
|
d: 'M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
name: 'GitHub',
|
||||||
|
href: '#',
|
||||||
|
icon: defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
|
||||||
|
h('path', {
|
||||||
|
'fill-rule': 'evenodd',
|
||||||
|
d: 'M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z',
|
||||||
|
'clip-rule': 'evenodd',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
dribbble: {
|
||||||
|
name: 'Dribbble',
|
||||||
|
href: '#',
|
||||||
|
icon: defineComponent({
|
||||||
|
render: () =>
|
||||||
|
h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
|
||||||
|
h('path', {
|
||||||
|
'fill-rule': 'evenodd',
|
||||||
|
d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2zm6.605 4.61a8.502 8.502 0 011.93 5.314c-.281-.054-3.101-.629-5.943-.271-.065-.141-.12-.293-.184-.445a25.416 25.416 0 00-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362zM12 3.475c2.17 0 4.154.813 5.662 2.148-.152.216-1.443 1.941-4.48 3.08-1.399-2.57-2.95-4.675-3.189-5A8.687 8.687 0 0112 3.475zm-3.633.803a53.896 53.896 0 013.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.581 8.581 0 014.729-5.975zM3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215.25.477.477.965.694 1.453-.109.033-.228.065-.336.098-4.404 1.42-6.747 5.303-6.942 5.629a8.522 8.522 0 01-2.19-5.705zM12 20.547a8.482 8.482 0 01-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337.022-.01.033-.01.054-.022a35.318 35.318 0 011.823 6.475 8.4 8.4 0 01-3.341.684zm4.761-1.465c-.086-.52-.542-3.015-1.659-6.084 2.679-.423 5.022.271 5.314.369a8.468 8.468 0 01-3.655 5.715z',
|
||||||
|
'clip-rule': 'evenodd',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
476
inspond-nuxt-safekiso/base/components/TopNavBar1.vue
Normal file
476
inspond-nuxt-safekiso/base/components/TopNavBar1.vue
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<Disclosure v-slot="{ open }" as="nav" class="bg-white shadow">
|
||||||
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex h-16 justify-between">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex flex-shrink-0 items-center">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
@click="handleItemClick($event, null)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="block h-8 w-auto lg:hidden"
|
||||||
|
:src="siteLogoUrl"
|
||||||
|
alt="site logo image"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
class="hidden h-8 w-auto lg:block"
|
||||||
|
:src="siteLogoUrl"
|
||||||
|
alt="site logo image"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||||
|
<!-- Current: "border-indigo-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" -->
|
||||||
|
<template
|
||||||
|
v-for="item in currentMenu['main']"
|
||||||
|
:key="item.idx"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="item.subs == undefined"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="
|
||||||
|
isCurrentMenu(item)
|
||||||
|
? 'inline-flex items-center border-b-2 border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900'
|
||||||
|
: 'inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700'
|
||||||
|
"
|
||||||
|
@click="handleItemClick($event, item)"
|
||||||
|
>{{ item['title'] }}</a
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:class="
|
||||||
|
isCurrentMenu(item)
|
||||||
|
? 'inline-flex items-center border-b-2 border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900'
|
||||||
|
: 'inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- dropdown -->
|
||||||
|
<Menu as="div" class="relative ml-3">
|
||||||
|
<div>
|
||||||
|
<MenuButton class="flex">
|
||||||
|
{{ item['title'] }}
|
||||||
|
<svg
|
||||||
|
x-state:on="Item active"
|
||||||
|
x-state:off="Item inactive"
|
||||||
|
class="h-5 w-5 group-hover:text-gray-500 text-gray-400"
|
||||||
|
x-bind:class="{ 'text-gray-600': flyoutMenuOpen, 'text-gray-400': !flyoutMenuOpen }"
|
||||||
|
x-description="Heroicon name: chevron-down"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</MenuButton>
|
||||||
|
</div>
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition ease-out duration-200"
|
||||||
|
enter-from-class="transform opacity-0 scale-95"
|
||||||
|
enter-to-class="transform opacity-100 scale-100"
|
||||||
|
leave-active-class="transition ease-in duration-75"
|
||||||
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
|
leave-to-class="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<MenuItems
|
||||||
|
class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
v-for="subItem in item['subs']"
|
||||||
|
v-slot="{ active }"
|
||||||
|
:key="subItem.idx"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="[
|
||||||
|
active
|
||||||
|
? 'bg-gray-100'
|
||||||
|
: '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-700',
|
||||||
|
]"
|
||||||
|
@click="
|
||||||
|
handleItemClick(
|
||||||
|
$event,
|
||||||
|
subItem
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>{{ subItem.title }}</a
|
||||||
|
>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItems>
|
||||||
|
</transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-for="item in currentMenu['sub']"
|
||||||
|
:key="item.idx"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="item.subs == undefined"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="
|
||||||
|
isCurrentMenu(item)
|
||||||
|
? 'inline-flex items-center border-b-2 border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900'
|
||||||
|
: 'inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700'
|
||||||
|
"
|
||||||
|
@click="handleItemClick($event, item)"
|
||||||
|
>{{ item['title'] }}</a
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:class="
|
||||||
|
isCurrentMenu(item)
|
||||||
|
? 'inline-flex items-center border-b-2 border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900'
|
||||||
|
: 'inline-flex items-center border-b-2 border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- dropdown -->
|
||||||
|
<Menu as="div" class="relative ml-3">
|
||||||
|
<div>
|
||||||
|
<MenuButton class="flex">
|
||||||
|
{{ item['title'] }}
|
||||||
|
<svg
|
||||||
|
x-state:on="Item active"
|
||||||
|
x-state:off="Item inactive"
|
||||||
|
class="h-5 w-5 group-hover:text-gray-500 text-gray-400"
|
||||||
|
x-bind:class="{ 'text-gray-600': flyoutMenuOpen, 'text-gray-400': !flyoutMenuOpen }"
|
||||||
|
x-description="Heroicon name: chevron-down"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</MenuButton>
|
||||||
|
</div>
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition ease-out duration-200"
|
||||||
|
enter-from-class="transform opacity-0 scale-95"
|
||||||
|
enter-to-class="transform opacity-100 scale-100"
|
||||||
|
leave-active-class="transition ease-in duration-75"
|
||||||
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
|
leave-to-class="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<MenuItems
|
||||||
|
class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
v-for="subItem in item['subs']"
|
||||||
|
v-slot="{ active }"
|
||||||
|
:key="subItem.idx"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="[
|
||||||
|
active
|
||||||
|
? 'bg-gray-100'
|
||||||
|
: '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-700',
|
||||||
|
]"
|
||||||
|
@click="
|
||||||
|
handleItemClick(
|
||||||
|
$event,
|
||||||
|
subItem
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>{{ subItem.title }}</a
|
||||||
|
>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItems>
|
||||||
|
</transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:ml-6 sm:flex sm:items-center">
|
||||||
|
<button
|
||||||
|
v-if="false"
|
||||||
|
type="button"
|
||||||
|
class="rounded-full bg-white p-1 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<span class="sr-only">View notifications</span>
|
||||||
|
<BellIcon class="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Profile dropdown -->
|
||||||
|
<Menu as="div" class="relative ml-3">
|
||||||
|
<div>
|
||||||
|
<MenuButton
|
||||||
|
class="flex rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open user menu</span>
|
||||||
|
<BaseUserProfileImage :image-size="2" />
|
||||||
|
</MenuButton>
|
||||||
|
</div>
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition ease-out duration-200"
|
||||||
|
enter-from-class="transform opacity-0 scale-95"
|
||||||
|
enter-to-class="transform opacity-100 scale-100"
|
||||||
|
leave-active-class="transition ease-in duration-75"
|
||||||
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
|
leave-to-class="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<MenuItems
|
||||||
|
class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
v-if="isAuthenticated"
|
||||||
|
v-slot="{ active }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="[
|
||||||
|
active ? 'bg-gray-100' : '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-700',
|
||||||
|
]"
|
||||||
|
@click="moveToPath('/user/info')"
|
||||||
|
>설정</a
|
||||||
|
>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem v-slot="{ active }">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="[
|
||||||
|
active ? 'bg-gray-100' : '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-700',
|
||||||
|
]"
|
||||||
|
@click="doSignInAndOut($event)"
|
||||||
|
>{{
|
||||||
|
isAuthenticated == true
|
||||||
|
? '로그아웃'
|
||||||
|
: '로그인'
|
||||||
|
}}</a
|
||||||
|
>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItems>
|
||||||
|
</transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
<div class="-mr-2 flex items-center sm:hidden">
|
||||||
|
<!-- Mobile menu button -->
|
||||||
|
<DisclosureButton
|
||||||
|
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<Bars3Icon
|
||||||
|
v-if="!open"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<XMarkIcon
|
||||||
|
v-else
|
||||||
|
class="block h-6 w-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</DisclosureButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DisclosurePanel class="sm:hidden">
|
||||||
|
<div class="space-y-1 pt-2 pb-3">
|
||||||
|
<!-- Current: "bg-indigo-50 border-indigo-500 text-indigo-700", Default: "border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700" -->
|
||||||
|
|
||||||
|
<template v-for="item in currentMenu['main']" :key="item.idx">
|
||||||
|
<DisclosureButton
|
||||||
|
v-if="item.path != undefined"
|
||||||
|
as="a"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="
|
||||||
|
currentRoutePath == item.path
|
||||||
|
? 'block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700'
|
||||||
|
: 'block border-l-4 border-transparent py-2 pl-3 pr-4 text-base font-medium text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
|
||||||
|
"
|
||||||
|
@click="handleItemClick($event, item)"
|
||||||
|
>{{ item['title'] }}</DisclosureButton
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="
|
||||||
|
currentRoutePath == item.path
|
||||||
|
? 'block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700'
|
||||||
|
: 'block border-l-4 border-transparent py-2 pl-3 pr-4 text-base font-medium text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
|
||||||
|
"
|
||||||
|
>{{ item['title'] }}</a
|
||||||
|
>
|
||||||
|
<template v-for="subItem in item.subs" :key="subItem.idx">
|
||||||
|
<DisclosureButton
|
||||||
|
v-if="subItem.path != undefined"
|
||||||
|
as="a"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="
|
||||||
|
currentRoutePath == subItem.path
|
||||||
|
? 'block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700'
|
||||||
|
: 'block border-l-4 border-transparent py-2 pl-3 pr-4 text-base font-medium text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
|
||||||
|
"
|
||||||
|
class="ml-4"
|
||||||
|
@click="handleItemClick($event, subItem)"
|
||||||
|
>{{ subItem['title'] }}</DisclosureButton
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="
|
||||||
|
currentRoutePath == subItem.path
|
||||||
|
? 'block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700'
|
||||||
|
: 'block border-l-4 border-transparent py-2 pl-3 pr-4 text-base font-medium text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
|
||||||
|
"
|
||||||
|
>{{ subItem['title'] }}</a
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-for="item in currentMenu['sub']" :key="item.idx">
|
||||||
|
<DisclosureButton
|
||||||
|
as="a"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="
|
||||||
|
currentRoutePath == item.path
|
||||||
|
? 'block border-l-4 border-indigo-500 bg-indigo-50 py-2 pl-3 pr-4 text-base font-medium text-indigo-700'
|
||||||
|
: 'block border-l-4 border-transparent py-2 pl-3 pr-4 text-base font-medium text-gray-500 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-700'
|
||||||
|
"
|
||||||
|
@click="handleItemClick($event, item)"
|
||||||
|
>{{ item['title'] }}</DisclosureButton
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-200 pt-4 pb-3">
|
||||||
|
<div class="flex items-center px-4">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<BaseUserProfileImage :image-size="3" />
|
||||||
|
</div>
|
||||||
|
<div v-if="false" class="ml-3">
|
||||||
|
<div class="text-base font-medium text-gray-800">
|
||||||
|
Tom Cook
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-medium text-gray-500">
|
||||||
|
tom@example.com
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="false"
|
||||||
|
type="button"
|
||||||
|
class="ml-auto flex-shrink-0 rounded-full bg-white p-1 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<span class="sr-only">View notifications</span>
|
||||||
|
<BellIcon class="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 space-y-1">
|
||||||
|
<DisclosureButton
|
||||||
|
v-if="isAuthenticated"
|
||||||
|
as="a"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800"
|
||||||
|
@click="moveToPath('/user/info')"
|
||||||
|
>설정</DisclosureButton
|
||||||
|
>
|
||||||
|
<DisclosureButton
|
||||||
|
as="a"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800"
|
||||||
|
@click="doSignInAndOut($event)"
|
||||||
|
>{{
|
||||||
|
isAuthenticated == true ? '로그아웃' : '로그인'
|
||||||
|
}}</DisclosureButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DisclosurePanel>
|
||||||
|
</Disclosure>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Disclosure,
|
||||||
|
DisclosureButton,
|
||||||
|
DisclosurePanel,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuItems,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
import { Bars3Icon, BellIcon, XMarkIcon } from '@heroicons/vue/24/outline';
|
||||||
|
const route = useRoute();
|
||||||
|
const currentRoutePath = ref(route.path);
|
||||||
|
const siteLogoUrl = ref(_siteConfig.siteLogoUrl);
|
||||||
|
const currentMenu = ref(_crossCtl.menu);
|
||||||
|
|
||||||
|
const isAuthenticated = ref(_crossCtl.isAuthenticated);
|
||||||
|
|
||||||
|
function isCurrentMenu(menuItem) {
|
||||||
|
let finalResult = false;
|
||||||
|
|
||||||
|
if (menuItem.path == currentRoutePath.value) {
|
||||||
|
finalResult = true;
|
||||||
|
} else {
|
||||||
|
if (menuItem['subs'] != undefined) {
|
||||||
|
for (let i = 0; i < menuItem['subs'].length; i++) {
|
||||||
|
if (menuItem['subs'][i].path == currentRoutePath.value) {
|
||||||
|
finalResult = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleItemClick(e, item) {
|
||||||
|
if (item == null) {
|
||||||
|
navigateTo('/');
|
||||||
|
currentRoutePath.value = '/';
|
||||||
|
} else {
|
||||||
|
_crossCtl.moveToMenuItem(item);
|
||||||
|
currentRoutePath.value = item.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveToPath(path) {
|
||||||
|
navigateTo(path);
|
||||||
|
currentRoutePath.value = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doSignInAndOut(e) {
|
||||||
|
e.target.blur();
|
||||||
|
if (_crossCtl.isAuthenticated.value) {
|
||||||
|
const response = await _crossCtl.doComm('signout', '', {});
|
||||||
|
console.log('response=', response);
|
||||||
|
if (response['responseCode'] == 200) {
|
||||||
|
_crossCtl.setUserInfo({
|
||||||
|
isAdmin: false,
|
||||||
|
isApproved: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isHighLeveled: false,
|
||||||
|
isOp: false,
|
||||||
|
isSuperOp: false,
|
||||||
|
});
|
||||||
|
_crossCtl.setUserProfile({});
|
||||||
|
isAuthenticated.value = false;
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
alert(response['responseMessage']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigateTo({
|
||||||
|
path: '/user/signin',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
27
inspond-nuxt-safekiso/base/components/VueJsonPretty.ts
Normal file
27
inspond-nuxt-safekiso/base/components/VueJsonPretty.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineComponent, h, PropType } from 'vue';
|
||||||
|
|
||||||
|
import VueJsonPretty from 'vue-json-pretty';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'VueJsonPretty',
|
||||||
|
components: {
|
||||||
|
VueJsonPretty,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
default: 'res',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
return () =>
|
||||||
|
h(VueJsonPretty, {
|
||||||
|
path: props.path,
|
||||||
|
data: props.data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
130
inspond-nuxt-safekiso/base/layouts/SideNavbarAndFooter.vue
Normal file
130
inspond-nuxt-safekiso/base/layouts/SideNavbarAndFooter.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
This example requires updating your template:
|
||||||
|
|
||||||
|
```
|
||||||
|
<html class="h-full bg-gray-100">
|
||||||
|
<body class="h-full">
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
<div>
|
||||||
|
<TransitionRoot as="template" :show="sidebarOpen">
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
class="relative z-40 md:hidden bg-white"
|
||||||
|
@close="sidebarOpen = false"
|
||||||
|
>
|
||||||
|
<TransitionChild
|
||||||
|
as="template"
|
||||||
|
enter="transition-opacity ease-linear duration-300"
|
||||||
|
enter-from="opacity-0"
|
||||||
|
enter-to="opacity-100"
|
||||||
|
leave="transition-opacity ease-linear duration-300"
|
||||||
|
leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0"
|
||||||
|
>
|
||||||
|
<div class="fixed inset-0 bg-gray-600 bg-opacity-75" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 flex z-40">
|
||||||
|
<TransitionChild
|
||||||
|
as="template"
|
||||||
|
enter="transition ease-in-out duration-300 transform"
|
||||||
|
enter-from="-translate-x-full"
|
||||||
|
enter-to="translate-x-0"
|
||||||
|
leave="transition ease-in-out duration-300 transform"
|
||||||
|
leave-from="translate-x-0"
|
||||||
|
leave-to="-translate-x-full"
|
||||||
|
>
|
||||||
|
<DialogPanel
|
||||||
|
class="relative flex-1 flex flex-col max-w-xs w-full"
|
||||||
|
>
|
||||||
|
<TransitionChild
|
||||||
|
as="template"
|
||||||
|
enter="ease-in-out duration-300"
|
||||||
|
enter-from="opacity-0"
|
||||||
|
enter-to="opacity-100"
|
||||||
|
leave="ease-in-out duration-300"
|
||||||
|
leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0"
|
||||||
|
>
|
||||||
|
<div class="absolute top-0 right-0 -mr-12 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||||
|
@click="sidebarOpen = false"
|
||||||
|
>
|
||||||
|
<span class="sr-only"
|
||||||
|
>Close sidebar</span
|
||||||
|
>
|
||||||
|
<XMarkIcon
|
||||||
|
class="h-6 w-6 text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</TransitionChild>
|
||||||
|
<BaseNavSideBar1 :on-move="onMoveHandler" />
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
<div class="flex-shrink-0 w-14" aria-hidden="true">
|
||||||
|
<!-- Force sidebar to shrink to fit close icon -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
|
||||||
|
<!-- Static sidebar for desktop -->
|
||||||
|
<div class="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0">
|
||||||
|
<!-- Sidebar component, swap this element with another sidebar if you like -->
|
||||||
|
<div class="flex-1 flex flex-col min-h-0 bg-white">
|
||||||
|
<BaseNavSideBar1 :on-move="onMoveHandler" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="md:pl-64 flex flex-col flex-1">
|
||||||
|
<div
|
||||||
|
class="sticky top-0 z-10 md:hidden pl-1 pt-1 sm:pl-3 sm:pt-3 bg-gray-100"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="-ml-0.5 -mt-0.5 h-12 w-12 inline-flex items-center justify-center rounded-md text-gray-500 hover:text-gray-900"
|
||||||
|
@click="sidebarOpen = true"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open sidebar</span>
|
||||||
|
<Bars3Icon class="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<main class="flex-1">
|
||||||
|
<!-- Replace with your content -->
|
||||||
|
<div class="mt-5 sm:px-3 lg:px-5"><slot /></div>
|
||||||
|
<!-- /End replace -->
|
||||||
|
</main>
|
||||||
|
<BaseModal1 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
TransitionChild,
|
||||||
|
TransitionRoot,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
import { Bars3Icon, XMarkIcon } from '@heroicons/vue/24/outline';
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
class: 'h-full bg-gray-50',
|
||||||
|
},
|
||||||
|
bodyAttrs: {
|
||||||
|
class: 'h-full',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sidebarOpen = ref(false);
|
||||||
|
|
||||||
|
function onMoveHandler() {
|
||||||
|
sidebarOpen.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
14
inspond-nuxt-safekiso/base/layouts/TopNavbarAndFooter.vue
Normal file
14
inspond-nuxt-safekiso/base/layouts/TopNavbarAndFooter.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col min-h-screen">
|
||||||
|
<TopNavBar1 class="mb-3" />
|
||||||
|
<slot class="bg-gray-50 mt-5 sm:px-3 lg:px-5" />
|
||||||
|
<!-- It is cushion -->
|
||||||
|
<div class="flex-grow justify-center"></div>
|
||||||
|
<BaseModal1 />
|
||||||
|
<Footer1 class="mt-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TopNavBar1 from '../components/TopNavBar1.vue';
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col min-h-screen">
|
||||||
|
<TopNavBar1 class="mb-3" />
|
||||||
|
<!-- It is cushion -->
|
||||||
|
<div class="flex-grow justify-center"></div>
|
||||||
|
|
||||||
|
<slot class="bg-gray-50 mt-5 sm:px-3 lg:px-5" />
|
||||||
|
<!-- It is cushion -->
|
||||||
|
<div class="flex-grow justify-center"></div>
|
||||||
|
<BaseModal1 />
|
||||||
|
<Footer1 class="mt-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TopNavBar1 from '../components/TopNavBar1.vue';
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<body class="flex flex-col min-h-screen">
|
||||||
|
<header>
|
||||||
|
<TopNavBar1 class="mb-3" />
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<slot class="bg-gray-50 mt-5 sm:px-3 lg:px-5" />
|
||||||
|
<!-- It is cushion -->
|
||||||
|
<div class="flex-grow justify-center"></div>
|
||||||
|
<BaseModal1 />
|
||||||
|
<Footer1 class="mt-3" />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="sticky top-[100vh]"><StickyFooter /></footer>
|
||||||
|
</body>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TopNavBar1 from '../components/TopNavBar1.vue';
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col min-h-screen">
|
||||||
|
<TopNavBar1 class="mb-3" />
|
||||||
|
<slot class="bg-gray-50 mt-5 sm:px-3 lg:px-5" />
|
||||||
|
<!-- It is cushion -->
|
||||||
|
<div class="flex-grow justify-center"></div>
|
||||||
|
<BaseModal1 />
|
||||||
|
<Footer1 class="mt-3" />
|
||||||
|
<div class="mt-3 bg-yellow-50"></div>
|
||||||
|
<StickyFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TopNavBar1 from '../components/TopNavBar1.vue';
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col min-h-screen">
|
||||||
|
<TopNavBar1 class="mb-3" />
|
||||||
|
<!-- It is cushion -->
|
||||||
|
<div class="flex-grow justify-center"></div>
|
||||||
|
|
||||||
|
<slot class="bg-gray-50 mt-5 sm:px-3 lg:px-5" />
|
||||||
|
<!-- It is cushion -->
|
||||||
|
<div class="flex-grow justify-center"></div>
|
||||||
|
<BaseModal1 />
|
||||||
|
<Footer1 class="mt-3" />
|
||||||
|
<div class="mt-3 bg-yellow-50"></div>
|
||||||
|
<StickyFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TopNavBar1 from '../components/TopNavBar1.vue';
|
||||||
|
</script>
|
||||||
8
inspond-nuxt-safekiso/base/layouts/center.vue
Normal file
8
inspond-nuxt-safekiso/base/layouts/center.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<div class="items-center justify-center h-screen">
|
||||||
|
<slot />
|
||||||
|
<BaseModal1 />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
27
inspond-nuxt-safekiso/base/layouts/default.vue
Normal file
27
inspond-nuxt-safekiso/base/layouts/default.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLayout :name="layout">
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { _siteConfig } from '@/config/site';
|
||||||
|
const layout = _siteConfig.siteLayout;
|
||||||
|
|
||||||
|
/*
|
||||||
|
let modalOpendedFlag = false;
|
||||||
|
|
||||||
|
if (modalOpendedFlag == false) {
|
||||||
|
_crossCtl.openModal(
|
||||||
|
'info',
|
||||||
|
'베타 서비스 종료',
|
||||||
|
'지금은 정식 서비스 오픈 준비중입니다. 2023년 5월 10일까지의 베타 서비스를 마치고 2023년 6월중에 정식 서비스 오픈을 위해 지금은 서비스 준비중입니다. 베타 서비스 기간에 생성된 계정이나 API 키는 현재 사용하실 수 없습니다. 확인 버튼을 누르시면 보다 상세한 안내 페이지로 이동합니다.',
|
||||||
|
['확인'],
|
||||||
|
(btnIdx) => {
|
||||||
|
modalOpendedFlag = true;
|
||||||
|
navigateTo('/doc/intermissions');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
7
inspond-nuxt-safekiso/base/layouts/raw.vue
Normal file
7
inspond-nuxt-safekiso/base/layouts/raw.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
35
inspond-nuxt-safekiso/base/middleware/base.global.ts
Normal file
35
inspond-nuxt-safekiso/base/middleware/base.global.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
const responseJson = await _crossCtl.doComm('select', 'user', {});
|
||||||
|
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
_crossCtl.setAuthInfo(responseJson['data'][0]);
|
||||||
|
|
||||||
|
_crossCtl.setUserInfo(responseJson['data'][0]);
|
||||||
|
|
||||||
|
if (_crossCtl.isAuthenticated.value) {
|
||||||
|
const tmpProfile = responseJson['data'][0].userInfo.profile;
|
||||||
|
// console.log('huk', tmpProfile);
|
||||||
|
const profile = {
|
||||||
|
email: tmpProfile.infos.email,
|
||||||
|
displayName: tmpProfile.display_name,
|
||||||
|
photoUrl: tmpProfile.photo_url,
|
||||||
|
phone: tmpProfile.infos.phone ? tmpProfile.infos.phone : '',
|
||||||
|
memo: tmpProfile.infos.memo ? tmpProfile.infos.memo : '',
|
||||||
|
};
|
||||||
|
_crossCtl.setUserProfile(profile);
|
||||||
|
} else {
|
||||||
|
_crossCtl.setUserProfile({});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
console.log('from = ', from, ', to = ', to);
|
||||||
|
_crossCtl.setUserProfile({});
|
||||||
|
if (to.fullPath != '/') {
|
||||||
|
return throwError('#' + responseJson['responseCode']);
|
||||||
|
} else {
|
||||||
|
console.log('skip for google...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
12
inspond-nuxt-safekiso/base/middleware/check-auth-admin.ts
Normal file
12
inspond-nuxt-safekiso/base/middleware/check-auth-admin.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { _crossCtl } from '@/base/src/crossCtl';
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
if (_crossCtl.userInfo['isAuthenticated'] == false) {
|
||||||
|
alert('로그인이 필요합니다.');
|
||||||
|
return navigateTo('/user/signin');
|
||||||
|
} else if (_crossCtl.userInfo['isAdmin'] == false) {
|
||||||
|
return throwError('$401');
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
16
inspond-nuxt-safekiso/base/middleware/check-auth-op.ts
Normal file
16
inspond-nuxt-safekiso/base/middleware/check-auth-op.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { _crossCtl } from '@/base/src/crossCtl';
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
if (_crossCtl.userInfo['isAuthenticated'] == false) {
|
||||||
|
alert('로그인이 필요합니다.');
|
||||||
|
return navigateTo('/user/signin');
|
||||||
|
} else if (
|
||||||
|
_crossCtl.userInfo['isOp'] == false &&
|
||||||
|
_crossCtl.userInfo['isSuperOp'] == false &&
|
||||||
|
_crossCtl.userInfo['isAdmin'] == false
|
||||||
|
) {
|
||||||
|
return throwError('$401');
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
15
inspond-nuxt-safekiso/base/middleware/check-auth-super.ts
Normal file
15
inspond-nuxt-safekiso/base/middleware/check-auth-super.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { _crossCtl } from '@/base/src/crossCtl';
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
if (_crossCtl.userInfo['isAuthenticated'] == false) {
|
||||||
|
alert('로그인이 필요합니다.');
|
||||||
|
return navigateTo('/user/signin');
|
||||||
|
} else if (
|
||||||
|
_crossCtl.userInfo['isSuperOp'] == false &&
|
||||||
|
_crossCtl.userInfo['isAdmin'] == false
|
||||||
|
) {
|
||||||
|
return throwError('$401');
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
8
inspond-nuxt-safekiso/base/middleware/check-auth-user.ts
Normal file
8
inspond-nuxt-safekiso/base/middleware/check-auth-user.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
if (_crossCtl.userInfo['isAuthenticated'] == false) {
|
||||||
|
alert('로그인이 필요합니다.');
|
||||||
|
return navigateTo('/user/signin');
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
23
inspond-nuxt-safekiso/base/nuxt.config.ts
Normal file
23
inspond-nuxt-safekiso/base/nuxt.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineNuxtConfig } from 'nuxt';
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
ssr: false,
|
||||||
|
autoImports: {
|
||||||
|
dirs: ['src'],
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
transpile: ['@fawmi/vue-google-maps', '@headlessui/vue', 'chart.js'],
|
||||||
|
plugins: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
API_BASE_URL: process.env.API_BASE_URL,
|
||||||
|
GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
css: [
|
||||||
|
'vue-json-pretty/lib/styles.css',
|
||||||
|
'@vueup/vue-quill/dist/vue-quill.snow.css',
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Page head goes here -->
|
||||||
|
<div class="px-4 py-5 sm:px-6">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center flex-wrap sm:flex-nowrap"
|
||||||
|
>
|
||||||
|
<div class="">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ pageTitle }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
{{ pageDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 sm:px-6">
|
||||||
|
<!-- Content goes here -->
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<div
|
||||||
|
class="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6"
|
||||||
|
>
|
||||||
|
<div class="sm:col-span-1">
|
||||||
|
<label
|
||||||
|
for="boardId"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
게시판 아이디
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<input
|
||||||
|
id="boardId"
|
||||||
|
v-model="boardId"
|
||||||
|
type="text"
|
||||||
|
name="boardId"
|
||||||
|
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label
|
||||||
|
for="title"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
게시판 제목
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
v-model="title"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6">
|
||||||
|
<label
|
||||||
|
for="description"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
게시판 설명
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
v-model="description"
|
||||||
|
name="description"
|
||||||
|
rows="3"
|
||||||
|
style="resize: none"
|
||||||
|
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="flex mt-2 text-sm text-gray-500">
|
||||||
|
<QuestionMarkCircleIcon
|
||||||
|
class="mr-1 flex-shrink-0 h-5 w-5 text-gray-400 group-hover:text-gray-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
사용자들에게 표시되는 내용이니 신중하게 입력하세요.
|
||||||
|
빈칸으로 그냥 둘 수도 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sm:col-span-3">
|
||||||
|
<label
|
||||||
|
for="readLevelMin"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
읽기제한
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<select
|
||||||
|
id="readLevelMin"
|
||||||
|
v-model="readLevelMin"
|
||||||
|
name="readLevelMin"
|
||||||
|
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
<option value="5">관리자 이상</option>
|
||||||
|
<option value="4">자격 사용자 이상</option>
|
||||||
|
<option value="0">로그인 사용자 이상</option>
|
||||||
|
<option value="-1">익명 사용자 이상</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sm:col-span-3">
|
||||||
|
<label
|
||||||
|
for="writeLevelMin"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
쓰기제한
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<select
|
||||||
|
id="writeLevelMin"
|
||||||
|
v-model="writeLevelMin"
|
||||||
|
name="writeLevelMin"
|
||||||
|
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
<option value="5">관리자 이상</option>
|
||||||
|
<option value="4">자격 사용자 이상</option>
|
||||||
|
<option value="0">로그인 사용자 이상</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sm:col-span-6">
|
||||||
|
<fieldset>
|
||||||
|
<legend class="sr-only">기타 옵션</legend>
|
||||||
|
<div
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
기타 옵션
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 space-y-4">
|
||||||
|
<div class="relative flex items-start">
|
||||||
|
<div class="flex h-5 items-center">
|
||||||
|
<input
|
||||||
|
id="commentEnabled"
|
||||||
|
v-model="commentEnabled"
|
||||||
|
name="commentEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm">
|
||||||
|
<label
|
||||||
|
for="commentEnabled"
|
||||||
|
class="font-medium text-gray-700"
|
||||||
|
>댓글 기능</label
|
||||||
|
>
|
||||||
|
<p class="text-gray-500">
|
||||||
|
쓰기 권한이 있는 사용자들은 댓글을
|
||||||
|
보고 쓸 수 있고, 읽기 권한이 있는
|
||||||
|
사용자들은 볼 수 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex items-start">
|
||||||
|
<div class="flex h-5 items-center">
|
||||||
|
<input
|
||||||
|
id="attachmentEnabled"
|
||||||
|
v-model="attachmentEnabled"
|
||||||
|
name="attachmentEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm">
|
||||||
|
<label
|
||||||
|
for="attachmentEnabled"
|
||||||
|
class="font-medium text-gray-700"
|
||||||
|
>파일 첨부</label
|
||||||
|
>
|
||||||
|
<p class="text-gray-500">
|
||||||
|
쓰기 권한이 있는 사용자들은 파일을
|
||||||
|
첨부할 수 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex items-start">
|
||||||
|
<div class="flex h-5 items-center">
|
||||||
|
<input
|
||||||
|
id="agoEnabled"
|
||||||
|
v-model="agoEnabled"
|
||||||
|
name="agoEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm">
|
||||||
|
<label
|
||||||
|
for="agoEnabled"
|
||||||
|
class="font-medium text-gray-700"
|
||||||
|
>작성일 축약 표시</label
|
||||||
|
>
|
||||||
|
<p class="text-gray-500">
|
||||||
|
작성일을 날짜와 시간 모두 표시하지
|
||||||
|
않고 짧게 표시합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6">
|
||||||
|
<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"
|
||||||
|
style="resize: none"
|
||||||
|
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="flex mt-2 text-sm text-gray-500">
|
||||||
|
<QuestionMarkCircleIcon
|
||||||
|
class="mr-1 flex-shrink-0 h-5 w-5 text-gray-400 group-hover:text-gray-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
이 게시판에 대해서 관리 목적상 필요한 간단한 메모를
|
||||||
|
저장할 수 있습니다. 관리자에게만 표시됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-4 lg:px-4">
|
||||||
|
<!-- Footer Buttons goes here -->
|
||||||
|
|
||||||
|
<div class="mt-5 flex justify-between items-center flex-wrap">
|
||||||
|
<div class="ml-4 mt-4">
|
||||||
|
<button
|
||||||
|
v-if="currnetMode != 'new'"
|
||||||
|
class="ml-3 inline-flex items-center rounded-md border border-transparent bg-red-600 px-3 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
||||||
|
@click="doFooterAction(status == 0 ? '삭제' : '복구')"
|
||||||
|
>
|
||||||
|
{{ status == 0 ? '삭제' : '복구' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4 mt-4 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center rounded-md border border-transparent bg-indigo-100 px-3 py-2 text-sm font-medium leading-4 text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
@click="doFooterAction('닫기')"
|
||||||
|
>
|
||||||
|
{{ '닫기' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="ml-3 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="doFooterAction('저장')"
|
||||||
|
>
|
||||||
|
{{ '저장' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { QuestionMarkCircleIcon } from '@heroicons/vue/24/solid';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const pageTitle = '새로운 게시판 생성';
|
||||||
|
const pageDescription = '새로운 게시판 생성 정보를 입력하세요.';
|
||||||
|
|
||||||
|
// 해당 페이지 우측 상단에 표시될 액션 버튼들
|
||||||
|
const headingActions = [];
|
||||||
|
|
||||||
|
const bid = ref('');
|
||||||
|
const boardId = ref('');
|
||||||
|
const title = ref('');
|
||||||
|
const description = ref('');
|
||||||
|
const readLevelMin = ref(5);
|
||||||
|
const writeLevelMin = ref(5);
|
||||||
|
const commentEnabled = ref(false);
|
||||||
|
const attachmentEnabled = ref(false);
|
||||||
|
const agoEnabled = ref(false);
|
||||||
|
const memo = ref('');
|
||||||
|
const status = ref(0);
|
||||||
|
|
||||||
|
function doHeadingAction(tag) {
|
||||||
|
console.log('on doHeadingAction(), tag=', tag);
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
default:
|
||||||
|
alert('unhandled heading action. tag = ' + tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doFooterAction(tag) {
|
||||||
|
console.log('111 on doFooterAction(), tag=', tag);
|
||||||
|
switch (tag) {
|
||||||
|
case '저장':
|
||||||
|
updateContent();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '닫기':
|
||||||
|
router.back();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '삭제':
|
||||||
|
deleteContent();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '복구':
|
||||||
|
reviveContent();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
alert('unhandled footer action. tag = ' + tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteContent() {
|
||||||
|
const responseJson = await _crossCtl.doComm('delete', 'board:info', {
|
||||||
|
hero: bid.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
status.value = 4;
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
// router.back();
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reviveContent() {
|
||||||
|
status.value = 0;
|
||||||
|
updateContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateContent() {
|
||||||
|
if (boardId.value.trim() == '') {
|
||||||
|
alert('게시판 아이디를 입력해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title.value.trim() == '') {
|
||||||
|
alert('게시판 제목을 입력해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
bid.value == '' ? 'insert' : 'update',
|
||||||
|
'board:info',
|
||||||
|
bid.value == ''
|
||||||
|
? {
|
||||||
|
boardId: boardId.value,
|
||||||
|
title: title.value,
|
||||||
|
description: description.value,
|
||||||
|
readLevelMin: readLevelMin.value,
|
||||||
|
writeLevelMin: writeLevelMin.value,
|
||||||
|
commentEnabled: commentEnabled.value,
|
||||||
|
attachmentEnabled: attachmentEnabled.value,
|
||||||
|
agoEnabled: agoEnabled.value,
|
||||||
|
memo: memo.value,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: bid.value,
|
||||||
|
boardId: boardId.value,
|
||||||
|
title: title.value,
|
||||||
|
description: description.value,
|
||||||
|
readLevelMin: readLevelMin.value,
|
||||||
|
writeLevelMin: writeLevelMin.value,
|
||||||
|
commentEnabled: commentEnabled.value,
|
||||||
|
attachmentEnabled: attachmentEnabled.value,
|
||||||
|
agoEnabled: agoEnabled.value,
|
||||||
|
memo: memo.value,
|
||||||
|
status: status.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('huk responseJson = ', responseJson);
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
if (responseJson['responseMessage'].startsWith('ER_DUP_ENTRY: ')) {
|
||||||
|
alert('게시판 아이디가 이미 존재하고 있습니다.');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// router.back();
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const currnetMode = ref('');
|
||||||
|
|
||||||
|
if (route.params.mode instanceof Array) {
|
||||||
|
console.log('huk 1');
|
||||||
|
|
||||||
|
if (route.params.mode.length != 1) {
|
||||||
|
console.log('huk 2');
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
console.log('huk 3');
|
||||||
|
currnetMode.value = route.params.mode[0];
|
||||||
|
switch (currnetMode.value) {
|
||||||
|
case 'new':
|
||||||
|
case 'edit':
|
||||||
|
console.log('huk 4');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throwError('$404');
|
||||||
|
console.log('huk 5');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (route.params.mode == '' && route.params._bid == 'new') {
|
||||||
|
currnetMode.value = 'new';
|
||||||
|
} else {
|
||||||
|
throwError('$404');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('huk 7');
|
||||||
|
|
||||||
|
if (currnetMode.value == '') {
|
||||||
|
console.log('missing params...');
|
||||||
|
} else {
|
||||||
|
console.log('mode = ', currnetMode.value);
|
||||||
|
|
||||||
|
if (currnetMode.value == 'edit') {
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
'select',
|
||||||
|
'board:info:all',
|
||||||
|
{
|
||||||
|
hero: route.params._bid,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
if (responseJson['data'].length != 1) {
|
||||||
|
alert(
|
||||||
|
'게시판 아이디가 잘못되었습니다. 이전 화면으로 돌아 갑니다.'
|
||||||
|
);
|
||||||
|
navigateTo('/admin/board/list');
|
||||||
|
} else {
|
||||||
|
const targetData = responseJson['data'][0];
|
||||||
|
|
||||||
|
bid.value = targetData['bid'];
|
||||||
|
boardId.value = targetData['id'];
|
||||||
|
title.value = targetData['title'];
|
||||||
|
description.value = targetData['description'];
|
||||||
|
readLevelMin.value = targetData['read_level_min'];
|
||||||
|
writeLevelMin.value = targetData['write_level_min'];
|
||||||
|
commentEnabled.value = targetData['comment_enabled'] == 1;
|
||||||
|
attachmentEnabled.value = targetData['attachment_enabled'] == 1;
|
||||||
|
agoEnabled.value = targetData['ago_enabled'] == 1;
|
||||||
|
memo.value = targetData['memo'];
|
||||||
|
status.value = targetData['status'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const targetBoardInfo = await _crossCtl.getBoardInfo(route);
|
||||||
|
|
||||||
|
// console.log('huk targetBoardInfo = ', targetBoardInfo);
|
||||||
|
console.log('huk params = ', route.params);
|
||||||
|
</script>
|
||||||
233
inspond-nuxt-safekiso/base/pages/admin/board/list.vue
Normal file
233
inspond-nuxt-safekiso/base/pages/admin/board/list.vue
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Page head goes here -->
|
||||||
|
<div class="px-3 py-5">
|
||||||
|
<div
|
||||||
|
class="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"
|
||||||
|
>
|
||||||
|
<div class="ml-4 mt-4">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ pageTitle }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
{{ pageDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4 mt-4 flex-shrink-0">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-w mx-auto px-3">
|
||||||
|
<!-- Content goes here -->
|
||||||
|
|
||||||
|
<BaseTable2
|
||||||
|
:headings="listHeadings"
|
||||||
|
:actions="listActions"
|
||||||
|
: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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const listMode = ref(route.query.mode ? route.query.mode : '');
|
||||||
|
|
||||||
|
console.log('listMode.value=', listMode.value);
|
||||||
|
|
||||||
|
const pageTitle = ref(
|
||||||
|
listMode.value == 'trashcan'
|
||||||
|
? '게시판 관리 - 삭제 게시판 리스트'
|
||||||
|
: '게시판 관리 - 리스트'
|
||||||
|
);
|
||||||
|
const pageDescription = ref('게시판 관리 리스트 페이지 입니다.');
|
||||||
|
|
||||||
|
// 해당 페이지 우측 상단에 표시될 액션 버튼들
|
||||||
|
const headingActions = ['게시판 생성', '리스트 모드'];
|
||||||
|
|
||||||
|
// 리스트 쓰는 경에만 해당. 안되는 경우 모두 지울것.
|
||||||
|
const listSource = 'list';
|
||||||
|
const listTarget = ref('');
|
||||||
|
const listActions = ['보기', '수정'];
|
||||||
|
const actionKey = 'id';
|
||||||
|
|
||||||
|
const listHeadings = [
|
||||||
|
{
|
||||||
|
title: '아이디',
|
||||||
|
widthRatio: '10',
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '제목',
|
||||||
|
widthRatio: '25',
|
||||||
|
key: 'title',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '설명',
|
||||||
|
widthRatio: '40',
|
||||||
|
key: 'description',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '권한',
|
||||||
|
widthRatio: '10',
|
||||||
|
key: 'level_min',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '수정일',
|
||||||
|
widthRatio: '15',
|
||||||
|
key: 'updated',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const listData = ref([]);
|
||||||
|
|
||||||
|
const totalPageCount = ref(1);
|
||||||
|
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('YYYY/MM/DD A h:mm:ss');
|
||||||
|
// return $dayjs(val).format('YY/MM/DD');
|
||||||
|
} else if (key == 'level_min')
|
||||||
|
switch (val) {
|
||||||
|
case -1:
|
||||||
|
return '익명 사용자 이상';
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
return '로그인 사용자 이상';
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
return '확인 사용자 이상';
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
return '관리자 이상';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return val;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function doHeadingAction(tag) {
|
||||||
|
console.log('on doHeadingAction(), tag=', tag);
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
case '게시판 생성':
|
||||||
|
navigateTo('/admin/board/new');
|
||||||
|
break;
|
||||||
|
case '리스트 모드':
|
||||||
|
console.log('listMode.value=', '[' + listMode.value + ']');
|
||||||
|
if (listMode.value == 'trashcan') {
|
||||||
|
console.log('huk 1');
|
||||||
|
pageTitle.value = '게시판 관리 - 리스트';
|
||||||
|
await navigateTo('/admin/board/list', { replace: true });
|
||||||
|
listMode.value = '';
|
||||||
|
} else {
|
||||||
|
console.log('huk 2');
|
||||||
|
pageTitle.value = '게시판 관리 - 삭제 리스트';
|
||||||
|
await navigateTo('/admin/board/list?mode=trashcan', {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
listMode.value = 'trashcan';
|
||||||
|
}
|
||||||
|
pageMove(1);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
alert('unhandled heading action. tag = ' + tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// alert('headingAction : ' + tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAction(tag, target) {
|
||||||
|
console.log('on doAction(), tag=', tag, ', target=', target);
|
||||||
|
|
||||||
|
// alert('doAction : ' + tag + ', target = ' + target);
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
case '보기':
|
||||||
|
navigateTo('/board/' + target + '/list');
|
||||||
|
break;
|
||||||
|
case '수정':
|
||||||
|
navigateTo('/admin/board/edit/' + target);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageMove(targetPageIdex) {
|
||||||
|
// console.log('on pageMove(), targetPageIdex=', targetPageIdex);
|
||||||
|
currentPageNumber.value = targetPageIdex;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
listSource,
|
||||||
|
listMode.value == 'trashcan'
|
||||||
|
? 'admin:board:info:deactivated'
|
||||||
|
: 'admin:board:info:active',
|
||||||
|
{
|
||||||
|
start: (currentPageNumber.value - 1) * pageSize.value,
|
||||||
|
length: pageSize.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
currentPageNumber.value = responseJson['currentPageNumber'];
|
||||||
|
totalPageCount.value = responseJson['totalPageCount'];
|
||||||
|
pageSize.value = parseInt(responseJson['pageSize']);
|
||||||
|
recordsTotal.value = responseJson['recordsTotal'];
|
||||||
|
|
||||||
|
listData.value = responseJson['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
<!-- 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">
|
||||||
|
{{ pageDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||||
|
<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(activeListPath)"
|
||||||
|
>
|
||||||
|
활성 항목 리스트
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="currentTarget == 'inquiry'"
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
MagnifyingGlassCircleIcon,
|
||||||
|
} from '@heroicons/vue/24/solid';
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
|
||||||
|
const currentTarget = ref('notice');
|
||||||
|
|
||||||
|
const pageTitle = ref('제목');
|
||||||
|
const pageDescription = ref('설명');
|
||||||
|
|
||||||
|
const listActions = ref(['상세보기']);
|
||||||
|
const actionKey = ref('serial');
|
||||||
|
const listKeys = ref(['serial', 'uid', 'name', 'domain', 'email', 'role']);
|
||||||
|
|
||||||
|
const listHeadings = ref([]);
|
||||||
|
|
||||||
|
const doActionTargetName = 'admin-support-target-edit';
|
||||||
|
let listSource = 'list';
|
||||||
|
let listTarget = 'dummy';
|
||||||
|
let activeListPath = '/admin/key/deleted';
|
||||||
|
|
||||||
|
let makeNewTargetPath = 'admin-support-notice-new';
|
||||||
|
|
||||||
|
const inquiryListOptionTags = ['대기중', '검토중', '답변완료', '삭제'];
|
||||||
|
|
||||||
|
if (route.params.target instanceof Array) {
|
||||||
|
if (route.params.target.length != 1) {
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
route.params.target[0] != 'notice' &&
|
||||||
|
route.params.target[0] != 'faq' &&
|
||||||
|
route.params.target[0] != 'inquiry'
|
||||||
|
) {
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
currentTarget.value = route.params.target[0];
|
||||||
|
|
||||||
|
console.log('currentTarget.value=', currentTarget.value);
|
||||||
|
switch (route.params.target[0]) {
|
||||||
|
case 'notice':
|
||||||
|
pageTitle.value = '삭제된 공지사항';
|
||||||
|
pageDescription.value =
|
||||||
|
'삭제된 공지사항을 보고 복구 합니다.';
|
||||||
|
|
||||||
|
listHeadings.value = [
|
||||||
|
{
|
||||||
|
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: '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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
|
||||||
|
key: 'title',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 lg: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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
|
||||||
|
key: 'updated',
|
||||||
|
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: 'created',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
listActions.value = ['상세보기'];
|
||||||
|
actionKey.value = 'serial';
|
||||||
|
listKeys.value = [
|
||||||
|
'serial',
|
||||||
|
'title',
|
||||||
|
'status',
|
||||||
|
'updated',
|
||||||
|
'created',
|
||||||
|
];
|
||||||
|
|
||||||
|
makeNewTargetPath = '/admin/support/notice/new';
|
||||||
|
listSource = 'list';
|
||||||
|
listTarget = 'notice:deleted';
|
||||||
|
|
||||||
|
activeListPath =
|
||||||
|
'/admin/support/' + route.params.target[0] + '/list';
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'faq':
|
||||||
|
pageTitle.value = '삭제된 자주 묻는 질문';
|
||||||
|
pageDescription.value = '삭제된 FAQ를 보고 복구 합니다.';
|
||||||
|
|
||||||
|
listHeadings.value = [
|
||||||
|
{
|
||||||
|
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: '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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
|
||||||
|
key: 'question',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 lg: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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
|
||||||
|
key: 'updated',
|
||||||
|
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: 'created',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
listActions.value = ['상세보기'];
|
||||||
|
actionKey.value = 'serial';
|
||||||
|
listKeys.value = [
|
||||||
|
'serial',
|
||||||
|
'question',
|
||||||
|
'status',
|
||||||
|
'updated',
|
||||||
|
'created',
|
||||||
|
];
|
||||||
|
|
||||||
|
makeNewTargetPath = '/admin/support/faq/new';
|
||||||
|
listSource = 'list';
|
||||||
|
listTarget = 'faq:deleted';
|
||||||
|
|
||||||
|
activeListPath =
|
||||||
|
'/admin/support/' + route.params.target[0] + '/list';
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'inquiry':
|
||||||
|
pageTitle.value = '처리 완료된 1:1 문의';
|
||||||
|
pageDescription.value =
|
||||||
|
'처리 완료된 1:1 문의 내용을 확인할 수 있습니다.';
|
||||||
|
|
||||||
|
listHeadings.value = [
|
||||||
|
{
|
||||||
|
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: '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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
|
||||||
|
key: 'name',
|
||||||
|
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: 'title',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 lg: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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
|
||||||
|
key: 'updated',
|
||||||
|
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: 'created',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
listActions.value = ['상세보기'];
|
||||||
|
actionKey.value = 'serial';
|
||||||
|
listKeys.value = [
|
||||||
|
'serial',
|
||||||
|
'name',
|
||||||
|
'title',
|
||||||
|
'status',
|
||||||
|
'updated',
|
||||||
|
'created',
|
||||||
|
];
|
||||||
|
|
||||||
|
makeNewTargetPath = '/admin/support/inquiry/new';
|
||||||
|
listSource = 'list';
|
||||||
|
listTarget = 'admin:inquiry:done';
|
||||||
|
|
||||||
|
activeListPath =
|
||||||
|
'/admin/support/' + route.params.target[0] + '/list';
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throwError('$404');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 if (key == 'status') {
|
||||||
|
if (currentTarget.value == 'inquiry') {
|
||||||
|
return inquiryListOptionTags[val];
|
||||||
|
} else {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
function doAction(tag, target) {
|
||||||
|
console.log('on doAction(), tag=', tag, ', target=', target);
|
||||||
|
navigateTo('/admin/support/' + currentTarget.value + '/edit/' + target);
|
||||||
|
/*
|
||||||
|
router.push({
|
||||||
|
name: doActionTargetName,
|
||||||
|
params: { hero: target, target: [currentTarget.value] },
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeNewOne() {
|
||||||
|
/*
|
||||||
|
router.push({
|
||||||
|
path: makeNewTargetPath,
|
||||||
|
params: {},
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
navigateTo({ path: makeNewTargetPath, params: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageMove(targetPageIdex) {
|
||||||
|
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
|
||||||
|
currentPageNumber.value = targetPageIdex;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
if (process.client) {
|
||||||
|
const responseJson = await _crossCtl.doComm(listSource, listTarget, {
|
||||||
|
start: (currentPageNumber.value - 1) * pageSize.value,
|
||||||
|
length: pageSize.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
currentPageNumber.value = responseJson['currentPageNumber'];
|
||||||
|
totalPageCount.value = responseJson['totalPageCount'];
|
||||||
|
pageSize.value = parseInt(responseJson['pageSize']);
|
||||||
|
recordsTotal.value = responseJson['recordsTotal'];
|
||||||
|
|
||||||
|
listData.value = responseJson['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,760 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="doUpdate">
|
||||||
|
<div class="sm:flex sm:items-center">
|
||||||
|
<div class="sm:flex-auto">
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900">
|
||||||
|
{{ newTitle }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm text-gray-700">
|
||||||
|
{{ newDescription }}
|
||||||
|
</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-2"></div>
|
||||||
|
<div v-if="currentTarget == 'inquiry'">
|
||||||
|
<form action="#" class="relative">
|
||||||
|
<div
|
||||||
|
class="border border-gray-300 rounded-lg shadow-sm overflow-hidden focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<label for="title" class="sr-only">Title</label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
v-model="targetTitle"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
class="block w-full border-0 pt-2.5 text-lg font-medium placeholder-gray-500 focus:ring-0"
|
||||||
|
placeholder="Title"
|
||||||
|
/>
|
||||||
|
<label for="description" class="sr-only">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
v-model="targetContent"
|
||||||
|
rows="8"
|
||||||
|
disabled
|
||||||
|
name="description"
|
||||||
|
class="block w-full border-0 py-0 resize-none placeholder-gray-500 focus:ring-0 sm:text-sm"
|
||||||
|
placeholder="Write a description..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Spacer element to match the height of the toolbar -->
|
||||||
|
<div aria-hidden="true">
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="h-9" />
|
||||||
|
</div>
|
||||||
|
<div class="h-px" />
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="py-px">
|
||||||
|
<div class="h-9" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 inset-x-px">
|
||||||
|
<!-- Actions: These are just examples to demonstrate the concept, replace/wire these up however makes sense for your project. -->
|
||||||
|
<div
|
||||||
|
class="flex flex-nowrap justify-end py-2 px-2 space-x-2 sm:px-3"
|
||||||
|
>
|
||||||
|
<Listbox
|
||||||
|
v-model="labelled"
|
||||||
|
as="div"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<ListboxLabel class="sr-only">
|
||||||
|
Add a label
|
||||||
|
</ListboxLabel>
|
||||||
|
<div class="relative">
|
||||||
|
<ListboxButton
|
||||||
|
disabled
|
||||||
|
class="relative inline-flex items-center rounded-full py-2 px-2 bg-gray-50 text-sm font-medium text-gray-500 whitespace-nowrap hover:bg-gray-100 sm:px-3"
|
||||||
|
>
|
||||||
|
<TagIcon
|
||||||
|
:class="[
|
||||||
|
labelled.value === null
|
||||||
|
? 'text-gray-300'
|
||||||
|
: 'text-gray-500',
|
||||||
|
'flex-shrink-0 h-5 w-5 sm:-ml-1',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
labelled.value === null
|
||||||
|
? ''
|
||||||
|
: 'text-gray-900',
|
||||||
|
'hidden truncate sm:ml-2 sm:block',
|
||||||
|
]"
|
||||||
|
>{{
|
||||||
|
labelled.value === null
|
||||||
|
? 'Label'
|
||||||
|
: labelled.name
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</ListboxButton>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
leave-active-class="transition ease-in duration-100"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<ListboxOptions
|
||||||
|
class="absolute right-0 z-10 mt-1 w-52 bg-white shadow max-h-56 rounded-lg py-3 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<ListboxOption
|
||||||
|
v-for="label in labels"
|
||||||
|
:key="label.value"
|
||||||
|
v-slot="{ active }"
|
||||||
|
as="template"
|
||||||
|
:value="label"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
active
|
||||||
|
? 'bg-gray-100'
|
||||||
|
: 'bg-white',
|
||||||
|
'cursor-default select-none relative py-2 px-3',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
class="block font-medium truncate"
|
||||||
|
>
|
||||||
|
{{ label.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
</ListboxOptions>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
<base-attachment-ctl1
|
||||||
|
:attachments="targetAttachmentFrom"
|
||||||
|
:read-only-flag="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="mt-5 text-sm text-gray-700">답변을 작성 하세요.</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border border-gray-300 rounded-lg shadow-sm overflow-hidden focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<label for="answer" class="sr-only">answer</label>
|
||||||
|
<textarea
|
||||||
|
id="answer"
|
||||||
|
v-model="targetAnswer"
|
||||||
|
:disabled="!(targetStatus == 0 || targetStatus == 1)"
|
||||||
|
rows="8"
|
||||||
|
name="answer"
|
||||||
|
class="m-1 mt-2 block w-full border-0 py-0 resize-none placeholder-gray-500 focus:ring-0 sm:text-sm"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<base-attachment-ctl1
|
||||||
|
:attachments="targetAttachmentTo"
|
||||||
|
:read-only-flag="false"
|
||||||
|
:update-attachments="updateAttachments"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<TabGroup v-slot="{ selectedIndex }">
|
||||||
|
<TabList class="flex items-center">
|
||||||
|
<Tab v-slot="{ selected }" as="template">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
selected
|
||||||
|
? 'text-gray-900 bg-gray-100 hover:bg-gray-200'
|
||||||
|
: 'text-gray-500 hover:text-gray-900 bg-white hover:bg-gray-100',
|
||||||
|
'px-3 py-1.5 border border-transparent text-sm font-medium rounded-md',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
입력
|
||||||
|
</button>
|
||||||
|
</Tab>
|
||||||
|
<Tab v-slot="{ selected }" as="template">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
selected
|
||||||
|
? 'text-gray-900 bg-gray-100 hover:bg-gray-200'
|
||||||
|
: 'text-gray-500 hover:text-gray-900 bg-white hover:bg-gray-100',
|
||||||
|
'ml-2 px-3 py-1.5 border border-transparent text-sm font-medium rounded-md',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
미리보기
|
||||||
|
</button>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<!-- These buttons are here simply as examples and don't actually do anything. -->
|
||||||
|
<div v-if="actionTarget == 'notice'">
|
||||||
|
<div
|
||||||
|
v-if="selectedIndex === 0"
|
||||||
|
class="ml-auto flex items-center space-x-5"
|
||||||
|
>
|
||||||
|
<Listbox
|
||||||
|
v-model="labelled"
|
||||||
|
as="div"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<ListboxLabel class="sr-only">
|
||||||
|
Add a label
|
||||||
|
</ListboxLabel>
|
||||||
|
<div class="relative">
|
||||||
|
<ListboxButton
|
||||||
|
class="relative inline-flex items-center rounded-full py-2 px-2 bg-gray-50 text-sm font-medium text-gray-500 whitespace-nowrap hover:bg-gray-100 sm:px-3"
|
||||||
|
>
|
||||||
|
<TagIcon
|
||||||
|
:class="[
|
||||||
|
labelled.value === null
|
||||||
|
? 'text-gray-300'
|
||||||
|
: 'text-gray-500',
|
||||||
|
'flex-shrink-0 h-5 w-5 sm:-ml-1',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
labelled.value === null
|
||||||
|
? ''
|
||||||
|
: 'text-gray-900',
|
||||||
|
'hidden truncate sm:ml-2 sm:block',
|
||||||
|
]"
|
||||||
|
>{{
|
||||||
|
labelled.value === null
|
||||||
|
? '라벨'
|
||||||
|
: labelled.name
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</ListboxButton>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
leave-active-class="transition ease-in duration-100"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<ListboxOptions
|
||||||
|
class="absolute right-0 z-10 mt-1 w-52 bg-white shadow max-h-56 rounded-lg py-3 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<ListboxOption
|
||||||
|
v-for="label in labels"
|
||||||
|
:key="label.value"
|
||||||
|
v-slot="{ active }"
|
||||||
|
as="template"
|
||||||
|
:value="label"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
active
|
||||||
|
? 'bg-gray-100'
|
||||||
|
: 'bg-white',
|
||||||
|
'cursor-default select-none relative py-2 px-3',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="block font-medium truncate"
|
||||||
|
>
|
||||||
|
{{ label.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
</ListboxOptions>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels class="mt-2">
|
||||||
|
<TabPanel class="p-0.5 -m-0.5 rounded-lg">
|
||||||
|
<div
|
||||||
|
class="border border-gray-300 rounded-lg shadow-sm overflow-hidden focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<label for="title" class="sr-only">제목</label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
v-model="targetTitle"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
class="block w-full border-0 pt-2.5 text-lg font-medium placeholder-gray-500 focus:ring-0"
|
||||||
|
placeholder="제목"
|
||||||
|
/>
|
||||||
|
<label for="content" class="sr-only">내용</label>
|
||||||
|
<textarea
|
||||||
|
id="content"
|
||||||
|
v-model="targetContent"
|
||||||
|
rows="20"
|
||||||
|
name="content"
|
||||||
|
class="block w-full border-0 py-0 resize-none placeholder-gray-500 focus:ring-0 sm:text-sm"
|
||||||
|
placeholder="내용..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel class="p-0.5 -m-0.5 rounded-lg">
|
||||||
|
<div class="border-b">
|
||||||
|
<div
|
||||||
|
class="mx-px mt-px px-3 pt-2 pb-12 text-sm leading-5 text-gray-800"
|
||||||
|
>
|
||||||
|
<div v-if="currentTarget == 'notice'">
|
||||||
|
<BaseNoticeItem1 :item="previewItem" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<BaseFaqItem1 :item="previewItem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</TabGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 flex justify-between">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
v-if="currentTarget != 'inquiry'"
|
||||||
|
type="button"
|
||||||
|
:class="
|
||||||
|
targetStatus == 0
|
||||||
|
? 'inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500'
|
||||||
|
: '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="doToggle"
|
||||||
|
>
|
||||||
|
{{ targetStatus == 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
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
targetStatus == 0 || targetStatus == 1 ? '저장' : '확인'
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/vue';
|
||||||
|
import {
|
||||||
|
Listbox,
|
||||||
|
ListboxButton,
|
||||||
|
ListboxLabel,
|
||||||
|
ListboxOption,
|
||||||
|
ListboxOptions,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
import { TagIcon, PaperClipIcon } from '@heroicons/vue/24/solid';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
let labels = [
|
||||||
|
{ name: '라벨 없음', value: null },
|
||||||
|
{ name: '공지', value: 'notice' },
|
||||||
|
{ name: '이벤트', value: 'event' },
|
||||||
|
// More items...
|
||||||
|
];
|
||||||
|
const labelled = ref(labels[0]);
|
||||||
|
|
||||||
|
const newTitle = ref('');
|
||||||
|
const newDescription = ref('');
|
||||||
|
|
||||||
|
const contentTitle = ref('');
|
||||||
|
const contentMessageGuide = ref('');
|
||||||
|
|
||||||
|
const currentTarget = ref('notice');
|
||||||
|
|
||||||
|
let actionTarget = 'notice';
|
||||||
|
|
||||||
|
const inPregressFlag = ref(false);
|
||||||
|
|
||||||
|
const targetTitle = ref('');
|
||||||
|
const targetContent = ref('');
|
||||||
|
const targetAttachmentFrom = ref([]);
|
||||||
|
const targetAnswer = ref('');
|
||||||
|
const targetAttachmentTo = ref([]);
|
||||||
|
const targetStatus = ref(0);
|
||||||
|
|
||||||
|
const previewItem = ref({ title: '', detail: '', created: '' });
|
||||||
|
|
||||||
|
let targetCreated = '';
|
||||||
|
|
||||||
|
function updateAttachments(newAttachments) {
|
||||||
|
console.log('newAttachments=', newAttachments);
|
||||||
|
targetAttachmentTo.value = newAttachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(targetTitle, (newValue, oldValue) => {
|
||||||
|
previewItem.value =
|
||||||
|
actionTarget == 'notice'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
detail: targetContent.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: route.params.hero,
|
||||||
|
question: targetTitle.value,
|
||||||
|
answer: targetContent.value,
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(targetContent, (newValue, oldValue) => {
|
||||||
|
previewItem.value =
|
||||||
|
actionTarget == 'notice'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
detail: targetContent.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: route.params.hero,
|
||||||
|
question: targetTitle.value,
|
||||||
|
answer: targetContent.value,
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(labelled, (newValue, oldValue) => {
|
||||||
|
previewItem.value =
|
||||||
|
actionTarget == 'notice'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
detail: targetContent.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: route.params.hero,
|
||||||
|
question: targetTitle.value,
|
||||||
|
answer: targetContent.value,
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function doToggle() {
|
||||||
|
if (targetStatus.value == 0) {
|
||||||
|
const responseJson = await _crossCtl.doComm('delete', actionTarget, {
|
||||||
|
hero: route.params.hero,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
targetStatus.value = 4;
|
||||||
|
alert('ok');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targetStatus.value = 0;
|
||||||
|
/*
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
'update',
|
||||||
|
actionTarget,
|
||||||
|
actionTarget == 'notice'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
detail: targetContent.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: route.params.hero,
|
||||||
|
question: targetTitle.value,
|
||||||
|
answer: targetContent.value,
|
||||||
|
attachmentTo: targetAttachmentTo.value,
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
'update',
|
||||||
|
actionTarget == 'inquiry' ? 'inquiry:admin' : actionTarget,
|
||||||
|
actionTarget == 'notice'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
detail: targetContent.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
}
|
||||||
|
: actionTarget == 'faq'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
question: targetTitle.value,
|
||||||
|
answer: targetContent.value,
|
||||||
|
status: targetStatus.value,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: route.params.hero,
|
||||||
|
answer: targetAnswer.value,
|
||||||
|
attachmentTo: targetAttachmentTo.value,
|
||||||
|
memo: '',
|
||||||
|
status: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
targetStatus.value = 0;
|
||||||
|
|
||||||
|
alert('ok');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('huk route.params.target=', route.params.target);
|
||||||
|
|
||||||
|
if (route.params.target instanceof Array) {
|
||||||
|
if (route.params.target.length != 1) {
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
route.params.target[0] != 'notice' &&
|
||||||
|
route.params.target[0] != 'faq' &&
|
||||||
|
route.params.target[0] != 'inquiry'
|
||||||
|
) {
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
currentTarget.value = route.params.target[0];
|
||||||
|
actionTarget = route.params.target[0];
|
||||||
|
switch (route.params.target[0]) {
|
||||||
|
case 'notice':
|
||||||
|
newTitle.value = '공지 수정';
|
||||||
|
newDescription.value =
|
||||||
|
'본문 중 html태그는 변환되어 저장되고 표시되므로 미리보기를 통해 의도한 대로 보여지는지 미리 확인해 주세요.';
|
||||||
|
|
||||||
|
contentTitle.value = '공지 제목';
|
||||||
|
contentMessageGuide.value = '공지 내용';
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'faq':
|
||||||
|
newTitle.value = 'FAQ 수정';
|
||||||
|
newDescription.value =
|
||||||
|
'본문 중 html태그는 변환되어 저장되고 표시되므로 미리보기를 통해 의도한 대로 보여지는지 미리 확인해 주세요.';
|
||||||
|
|
||||||
|
contentTitle.value = '질문';
|
||||||
|
contentMessageGuide.value = '답변';
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'inquiry':
|
||||||
|
newTitle.value = '1:1 문의 처리';
|
||||||
|
newDescription.value =
|
||||||
|
'1:1 문의에 답을 입력하면 상태가 즉시 답변 완료로 변하지만, 내용은 추가 수정할 수 있습니다.';
|
||||||
|
|
||||||
|
contentTitle.value = '질문';
|
||||||
|
contentMessageGuide.value = '답변';
|
||||||
|
|
||||||
|
labels = [
|
||||||
|
{ name: '라벨 없음', value: null },
|
||||||
|
{ name: '사이트 이용', value: 'site' },
|
||||||
|
{ name: 'API 문의', value: 'api' },
|
||||||
|
{ name: '기타', value: 'etc' },
|
||||||
|
// More items...
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('route.params=', route.params);
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
'select',
|
||||||
|
currentTarget.value,
|
||||||
|
{
|
||||||
|
hero: route.params.hero,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('huk responseJson = ', responseJson);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
console.log(responseJson['data']);
|
||||||
|
|
||||||
|
if (actionTarget == 'notice') {
|
||||||
|
targetTitle.value = responseJson['data'][0]['title'];
|
||||||
|
targetContent.value = responseJson['data'][0]['detail'];
|
||||||
|
|
||||||
|
const tmpFlags =
|
||||||
|
responseJson['data'][0]['flags'] != null
|
||||||
|
? responseJson['data'][0]['flags']
|
||||||
|
: '[]';
|
||||||
|
|
||||||
|
const flags = JSON.parse(tmpFlags);
|
||||||
|
for (let i = 0; i < flags.length; i++) {
|
||||||
|
const flag = flags[i];
|
||||||
|
for (let j = 0; j < labels.length; j++) {
|
||||||
|
if (flag == labels[j]['value']) {
|
||||||
|
labelled.value = labels[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (actionTarget == 'inquiry') {
|
||||||
|
targetTitle.value = responseJson['data'][0]['title'];
|
||||||
|
targetContent.value = responseJson['data'][0]['question'];
|
||||||
|
targetAttachmentFrom.value = JSON.parse(
|
||||||
|
responseJson['data'][0]['attachment_from']
|
||||||
|
);
|
||||||
|
targetAnswer.value = responseJson['data'][0]['answer'];
|
||||||
|
targetAttachmentTo.value = JSON.parse(
|
||||||
|
responseJson['data'][0]['attachment_to']
|
||||||
|
);
|
||||||
|
|
||||||
|
const tmpFlags =
|
||||||
|
responseJson['data'][0]['flags'] != null
|
||||||
|
? responseJson['data'][0]['flags']
|
||||||
|
: '[]';
|
||||||
|
|
||||||
|
const flags = JSON.parse(tmpFlags);
|
||||||
|
for (let i = 0; i < flags.length; i++) {
|
||||||
|
const flag = flags[i];
|
||||||
|
for (let j = 0; j < labels.length; j++) {
|
||||||
|
if (flag == labels[j]['value']) {
|
||||||
|
labelled.value = labels[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targetTitle.value = responseJson['data'][0]['question'];
|
||||||
|
targetContent.value = responseJson['data'][0]['answer'];
|
||||||
|
}
|
||||||
|
|
||||||
|
targetCreated = responseJson['data'][0]['created'];
|
||||||
|
|
||||||
|
targetStatus.value = responseJson['data'][0]['status'];
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throwError('$404');
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
async function doCancel() {
|
||||||
|
if (inPregressFlag.value == true) {
|
||||||
|
alert('이전 동작이 아직 진행중입니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doUpdate() {
|
||||||
|
if (inPregressFlag.value == true) {
|
||||||
|
alert('이전 동작이 아직 진행중입니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetTitle.value == '' || targetContent.value == '') {
|
||||||
|
alert('내용을 입력하셔야 합니다. ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
actionTarget == 'inquiry' &&
|
||||||
|
(targetAnswer.value == '' || targetAnswer.value == null)
|
||||||
|
) {
|
||||||
|
alert('답변 내용을 입력하셔야 합니다. ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('huk actionTarget = ', actionTarget);
|
||||||
|
console.log('huk targetAnswer.value = ', targetAnswer.value);
|
||||||
|
|
||||||
|
if (targetStatus.value == 2) {
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inPregressFlag.value = true;
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
'update',
|
||||||
|
actionTarget == 'inquiry' ? 'inquiry:admin' : actionTarget,
|
||||||
|
actionTarget == 'notice'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
detail: targetContent.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
}
|
||||||
|
: actionTarget == 'faq'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
question: targetTitle.value,
|
||||||
|
answer: targetContent.value,
|
||||||
|
status: targetStatus.value,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: route.params.hero,
|
||||||
|
answer: targetAnswer.value,
|
||||||
|
attachmentTo: targetAttachmentTo.value,
|
||||||
|
memo: '',
|
||||||
|
status: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
inPregressFlag.value = false;
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
alert('오류 : ' + responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,543 @@
|
|||||||
|
<!-- 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">
|
||||||
|
{{ pageDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentTarget == 'inquiry'" 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 value="all">전체</option>
|
||||||
|
<option value="wait">대기중</option>
|
||||||
|
<option value="done">답변완료</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 sm:mt-0 sm:ml-0 sm:flex-none">
|
||||||
|
<div
|
||||||
|
v-if="currentTarget == 'inquiry'"
|
||||||
|
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=""
|
||||||
|
@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 sm:block sm:text-sm border-gray-300"
|
||||||
|
placeholder=""
|
||||||
|
@keydown.enter.prevent="onEnterHandler()"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="currentTarget != 'inquiry'"
|
||||||
|
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(deletedListPath)"
|
||||||
|
>
|
||||||
|
삭제 항목 리스트
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="currentTarget == 'notice' || currentTarget == 'faq'"
|
||||||
|
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="makeNewOne"
|
||||||
|
>
|
||||||
|
새 항목 작성
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
MagnifyingGlassCircleIcon,
|
||||||
|
} from '@heroicons/vue/24/solid';
|
||||||
|
import { hueRotate } from 'tailwindcss/defaultTheme';
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
function onEnterHandler() {
|
||||||
|
doAction('search', searchKeyword.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetLevel = ref('all');
|
||||||
|
|
||||||
|
function onChangeLevel(e) {
|
||||||
|
console.log('targetLevel.value=', targetLevel.value);
|
||||||
|
|
||||||
|
listTarget = 'admin:inquiry:' + targetLevel.value;
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
const inquiryListOptionTags = ['대기중', '검토중', '답변완료', '삭제'];
|
||||||
|
const inquiryListOption = ref(0);
|
||||||
|
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
const currentTarget = ref('notice');
|
||||||
|
|
||||||
|
const pageTitle = ref('제목');
|
||||||
|
const pageDescription = ref('설명');
|
||||||
|
|
||||||
|
const listActions = ref(['상세보기']);
|
||||||
|
const actionKey = ref('serial');
|
||||||
|
const listKeys = ref(['serial', 'uid', 'name', 'domain', 'email', 'role']);
|
||||||
|
|
||||||
|
const listHeadings = ref([]);
|
||||||
|
|
||||||
|
const doActionTargetName = 'admin-support-target-edit';
|
||||||
|
let listSource = 'list';
|
||||||
|
let listTarget = 'admin:users:level:all';
|
||||||
|
let deletedListPath = '/admin/key/deleted';
|
||||||
|
|
||||||
|
let makeNewTargetPath = 'admin-support-notice-new';
|
||||||
|
|
||||||
|
if (route.params.target instanceof Array) {
|
||||||
|
if (route.params.target.length != 1) {
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
route.params.target[0] != 'notice' &&
|
||||||
|
route.params.target[0] != 'faq' &&
|
||||||
|
route.params.target[0] != 'inquiry'
|
||||||
|
) {
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
currentTarget.value = route.params.target[0];
|
||||||
|
switch (route.params.target[0]) {
|
||||||
|
case 'notice':
|
||||||
|
pageTitle.value = '공지사항';
|
||||||
|
pageDescription.value =
|
||||||
|
'공지사항을 작성하거나 수정, 삭제 합니다.';
|
||||||
|
|
||||||
|
listHeadings.value = [
|
||||||
|
{
|
||||||
|
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: '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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
|
||||||
|
key: 'title',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 lg: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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
|
||||||
|
key: 'updated',
|
||||||
|
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: 'created',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
listActions.value = ['상세보기'];
|
||||||
|
actionKey.value = 'serial';
|
||||||
|
listKeys.value = [
|
||||||
|
'serial',
|
||||||
|
'title',
|
||||||
|
'status',
|
||||||
|
'updated',
|
||||||
|
'created',
|
||||||
|
];
|
||||||
|
|
||||||
|
makeNewTargetPath = '/admin/support/notice/new';
|
||||||
|
listSource = 'list';
|
||||||
|
listTarget = 'notice:active';
|
||||||
|
|
||||||
|
deletedListPath =
|
||||||
|
'/admin/support/' + route.params.target[0] + '/deleted';
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'faq':
|
||||||
|
pageTitle.value = '자주 묻는 질문';
|
||||||
|
pageDescription.value =
|
||||||
|
'FAQ를 작성하거나 수정, 삭제 합니다.';
|
||||||
|
|
||||||
|
listHeadings.value = [
|
||||||
|
{
|
||||||
|
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: '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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
|
||||||
|
key: 'question',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 lg: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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
|
||||||
|
key: 'updated',
|
||||||
|
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: 'created',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
listActions.value = ['상세보기'];
|
||||||
|
actionKey.value = 'serial';
|
||||||
|
listKeys.value = [
|
||||||
|
'serial',
|
||||||
|
'question',
|
||||||
|
'status',
|
||||||
|
'updated',
|
||||||
|
'created',
|
||||||
|
];
|
||||||
|
|
||||||
|
makeNewTargetPath = '/admin/support/faq/new';
|
||||||
|
listSource = 'list';
|
||||||
|
listTarget = 'faq:active';
|
||||||
|
|
||||||
|
deletedListPath =
|
||||||
|
'/admin/support/' + route.params.target[0] + '/deleted';
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'inquiry':
|
||||||
|
pageTitle.value = '1:1 문의';
|
||||||
|
pageDescription.value =
|
||||||
|
'응답하지 않은 1:1 문의 내용을 보고 회신합니다.';
|
||||||
|
|
||||||
|
listHeadings.value = [
|
||||||
|
{
|
||||||
|
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: '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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
|
||||||
|
key: 'name',
|
||||||
|
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: 'title',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 lg: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: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
|
||||||
|
key: 'updated',
|
||||||
|
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: 'created',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass:
|
||||||
|
'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
listActions.value = ['상세보기'];
|
||||||
|
actionKey.value = 'serial';
|
||||||
|
listKeys.value = [
|
||||||
|
'serial',
|
||||||
|
'name',
|
||||||
|
'title',
|
||||||
|
'status',
|
||||||
|
'updated',
|
||||||
|
'created',
|
||||||
|
];
|
||||||
|
|
||||||
|
makeNewTargetPath = '/admin/support/inquiry/new';
|
||||||
|
listSource = 'list';
|
||||||
|
listTarget = 'admin:inquiry:all';
|
||||||
|
|
||||||
|
deletedListPath =
|
||||||
|
'/admin/support/' + route.params.target[0] + '/deleted';
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throwError('$404');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 if (key == 'status') {
|
||||||
|
if (currentTarget.value == 'inquiry') {
|
||||||
|
return inquiryListOptionTags[val];
|
||||||
|
} else {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
function doAction(tag, target) {
|
||||||
|
console.log('on doAction(), tag=', tag, ', target=', target);
|
||||||
|
|
||||||
|
if (tag == 'search') {
|
||||||
|
currentPageNumber.value = 1;
|
||||||
|
refresh();
|
||||||
|
} else {
|
||||||
|
navigateTo('/admin/support/' + currentTarget.value + '/edit/' + target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
router.push({
|
||||||
|
name: doActionTargetName,
|
||||||
|
params: { hero: target, target: [currentTarget.value] },
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeNewOne() {
|
||||||
|
/*
|
||||||
|
router.push({
|
||||||
|
path: makeNewTargetPath,
|
||||||
|
params: {},
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
navigateTo({ path: makeNewTargetPath, params: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageMove(targetPageIdex) {
|
||||||
|
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
|
||||||
|
currentPageNumber.value = targetPageIdex;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
if (process.client) {
|
||||||
|
const responseJson = await _crossCtl.doComm(listSource, listTarget, {
|
||||||
|
hero: searchKeyword.value,
|
||||||
|
start: (currentPageNumber.value - 1) * pageSize.value,
|
||||||
|
length: pageSize.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
currentPageNumber.value = responseJson['currentPageNumber'];
|
||||||
|
totalPageCount.value = responseJson['totalPageCount'];
|
||||||
|
pageSize.value = parseInt(responseJson['pageSize']);
|
||||||
|
recordsTotal.value = responseJson['recordsTotal'];
|
||||||
|
|
||||||
|
listData.value = responseJson['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
|
<form @submit.prevent="doCreate">
|
||||||
|
<div class="sm:flex sm:items-center">
|
||||||
|
<div class="sm:flex-auto">
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900">
|
||||||
|
{{ newTitle }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm text-gray-700">
|
||||||
|
{{ newDescription }}
|
||||||
|
</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-2"></div>
|
||||||
|
|
||||||
|
<TabGroup v-slot="{ selectedIndex }">
|
||||||
|
<TabList class="flex items-center">
|
||||||
|
<Tab v-slot="{ selected }" as="template">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
selected
|
||||||
|
? 'text-gray-900 bg-gray-100 hover:bg-gray-200'
|
||||||
|
: 'text-gray-500 hover:text-gray-900 bg-white hover:bg-gray-100',
|
||||||
|
'px-3 py-1.5 border border-transparent text-sm font-medium rounded-md',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
입력
|
||||||
|
</button>
|
||||||
|
</Tab>
|
||||||
|
<Tab v-slot="{ selected }" as="template">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
selected
|
||||||
|
? 'text-gray-900 bg-gray-100 hover:bg-gray-200'
|
||||||
|
: 'text-gray-500 hover:text-gray-900 bg-white hover:bg-gray-100',
|
||||||
|
'ml-2 px-3 py-1.5 border border-transparent text-sm font-medium rounded-md',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
미리보기
|
||||||
|
</button>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<!-- These buttons are here simply as examples and don't actually do anything. -->
|
||||||
|
<div
|
||||||
|
v-if="selectedIndex === 0"
|
||||||
|
class="ml-auto flex items-center space-x-5"
|
||||||
|
>
|
||||||
|
<div v-if="currentTarget == 'notice'">
|
||||||
|
<Listbox
|
||||||
|
v-model="labelled"
|
||||||
|
as="div"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<ListboxLabel class="sr-only">
|
||||||
|
Add a label
|
||||||
|
</ListboxLabel>
|
||||||
|
<div class="relative">
|
||||||
|
<ListboxButton
|
||||||
|
class="relative inline-flex items-center rounded-full py-2 px-2 bg-gray-50 text-sm font-medium text-gray-500 whitespace-nowrap hover:bg-gray-100 sm:px-3"
|
||||||
|
>
|
||||||
|
<TagIcon
|
||||||
|
:class="[
|
||||||
|
labelled.value === null
|
||||||
|
? 'text-gray-300'
|
||||||
|
: 'text-gray-500',
|
||||||
|
'flex-shrink-0 h-5 w-5 sm:-ml-1',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
labelled.value === null
|
||||||
|
? ''
|
||||||
|
: 'text-gray-900',
|
||||||
|
'hidden truncate sm:ml-2 sm:block',
|
||||||
|
]"
|
||||||
|
>{{
|
||||||
|
labelled.value === null
|
||||||
|
? '라벨'
|
||||||
|
: labelled.name
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</ListboxButton>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
leave-active-class="transition ease-in duration-100"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<ListboxOptions
|
||||||
|
class="absolute right-0 z-10 mt-1 w-52 bg-white shadow max-h-56 rounded-lg py-3 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<ListboxOption
|
||||||
|
v-for="label in labels"
|
||||||
|
:key="label.value"
|
||||||
|
v-slot="{ active }"
|
||||||
|
as="template"
|
||||||
|
:value="label"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
active
|
||||||
|
? 'bg-gray-100'
|
||||||
|
: 'bg-white',
|
||||||
|
'cursor-default select-none relative py-2 px-3',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
class="block font-medium truncate"
|
||||||
|
>
|
||||||
|
{{ label.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
</ListboxOptions>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels class="mt-2">
|
||||||
|
<TabPanel class="p-0.5 -m-0.5 rounded-lg">
|
||||||
|
<div
|
||||||
|
class="border border-gray-300 rounded-lg shadow-sm overflow-hidden focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<label for="title" class="sr-only">제목</label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
v-model="targetTitle"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
class="block w-full border-0 pt-2.5 text-lg font-medium placeholder-gray-500 focus:ring-0"
|
||||||
|
placeholder="제목"
|
||||||
|
/>
|
||||||
|
<label for="content" class="sr-only">내용</label>
|
||||||
|
<textarea
|
||||||
|
id="content"
|
||||||
|
v-model="targetContent"
|
||||||
|
rows="20"
|
||||||
|
name="content"
|
||||||
|
class="block w-full border-0 py-0 resize-none placeholder-gray-500 focus:ring-0 sm:text-sm"
|
||||||
|
placeholder="내용..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel class="p-0.5 -m-0.5 rounded-lg">
|
||||||
|
<div class="border-b">
|
||||||
|
<div
|
||||||
|
class="mx-px mt-px px-3 pt-2 pb-12 text-sm leading-5 text-gray-800"
|
||||||
|
>
|
||||||
|
<div v-if="currentTarget == 'notice'">
|
||||||
|
<BaseNoticeItem1 :item="previewItem" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<BaseFaqItem1 :item="previewItem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</TabGroup>
|
||||||
|
|
||||||
|
<div class="mt-2 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>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/vue';
|
||||||
|
import {
|
||||||
|
Listbox,
|
||||||
|
ListboxButton,
|
||||||
|
ListboxLabel,
|
||||||
|
ListboxOption,
|
||||||
|
ListboxOptions,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
import { TagIcon } from '@heroicons/vue/24/solid';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = [
|
||||||
|
{ name: '라벨 없음', value: null },
|
||||||
|
{ name: '공지', value: 'notice' },
|
||||||
|
{ name: '이벤트', value: 'event' },
|
||||||
|
// More items...
|
||||||
|
];
|
||||||
|
const labelled = ref(labels[0]);
|
||||||
|
|
||||||
|
const newTitle = ref('');
|
||||||
|
const newDescription = ref('');
|
||||||
|
|
||||||
|
const contentTitle = ref('');
|
||||||
|
const contentMessageGuide = ref('');
|
||||||
|
|
||||||
|
const currentTarget = ref('notice');
|
||||||
|
|
||||||
|
let actionTarget = 'notice';
|
||||||
|
|
||||||
|
const inPregressFlag = ref(false);
|
||||||
|
|
||||||
|
const targetTitle = ref('');
|
||||||
|
const targetContent = ref('');
|
||||||
|
const targetStatus = ref(0);
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const targetCreated = today.toISOString();
|
||||||
|
// console.log('targetCreated=', targetCreated);
|
||||||
|
|
||||||
|
const previewItem = ref({ title: '', detail: '', created: targetCreated });
|
||||||
|
|
||||||
|
watch(targetTitle, (newValue, oldValue) => {
|
||||||
|
previewItem.value =
|
||||||
|
actionTarget == 'notice'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
detail: targetContent.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: route.params.hero,
|
||||||
|
question: targetTitle.value,
|
||||||
|
answer: targetContent.value,
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(targetContent, (newValue, oldValue) => {
|
||||||
|
previewItem.value =
|
||||||
|
actionTarget == 'notice'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
detail: targetContent.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: route.params.hero,
|
||||||
|
question: targetTitle.value,
|
||||||
|
answer: targetContent.value,
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(labelled, (newValue, oldValue) => {
|
||||||
|
previewItem.value =
|
||||||
|
actionTarget == 'notice'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
detail: targetContent.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: route.params.hero,
|
||||||
|
question: targetTitle.value,
|
||||||
|
answer: targetContent.value,
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (route.params.target instanceof Array) {
|
||||||
|
if (route.params.target.length != 1) {
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
route.params.target[0] != 'notice' &&
|
||||||
|
route.params.target[0] != 'faq'
|
||||||
|
) {
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
currentTarget.value = route.params.target[0];
|
||||||
|
actionTarget = route.params.target[0];
|
||||||
|
switch (route.params.target[0]) {
|
||||||
|
case 'notice':
|
||||||
|
newTitle.value = '새 공지 작성';
|
||||||
|
newDescription.value =
|
||||||
|
'본문 중 html태그는 변환되어 저장되고 표시되므로 미리보기를 통해 의도한 대로 보여지는지 미리 확인해 주세요.';
|
||||||
|
|
||||||
|
contentTitle.value = '공지 제목';
|
||||||
|
contentMessageGuide.value = '공지 내용';
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'faq':
|
||||||
|
newTitle.value = '새 FAQ 작성';
|
||||||
|
newDescription.value =
|
||||||
|
'본문 중 html태그는 변환되어 저장되고 표시되므로 미리보기를 통해 의도한 대로 보여지는지 미리 확인해 주세요.';
|
||||||
|
|
||||||
|
contentTitle.value = '질문';
|
||||||
|
contentMessageGuide.value = '답변';
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throwError('$404');
|
||||||
|
}
|
||||||
|
const router = useRouter();
|
||||||
|
async function doCancel() {
|
||||||
|
if (inPregressFlag.value == true) {
|
||||||
|
alert('이전 동작이 아직 진행중입니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCreate() {
|
||||||
|
if (inPregressFlag.value == true) {
|
||||||
|
alert('이전 동작이 아직 진행중입니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetTitle.value == '' || targetContent.value == '') {
|
||||||
|
alert('내용을 입력하셔야 합니다. ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inPregressFlag.value = true;
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
'insert',
|
||||||
|
actionTarget,
|
||||||
|
actionTarget == 'notice'
|
||||||
|
? {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
detail: targetContent.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
hero: route.params.hero,
|
||||||
|
question: targetTitle.value,
|
||||||
|
answer: targetContent.value,
|
||||||
|
status: targetStatus.value,
|
||||||
|
created: targetCreated,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
inPregressFlag.value = false;
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
} else {
|
||||||
|
alert('오류 : ' + responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
58
inspond-nuxt-safekiso/base/pages/admin/support/index.vue
Normal file
58
inspond-nuxt-safekiso/base/pages/admin/support/index.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<!-- 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"
|
||||||
|
>
|
||||||
|
어드민 / 고객 지원
|
||||||
|
</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.push('/admin/support/notice/list')"
|
||||||
|
>
|
||||||
|
공지 리스트
|
||||||
|
</a>
|
||||||
|
,
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
@click="$router.push('/admin/support/faq/list')"
|
||||||
|
>
|
||||||
|
자주 묻는 질문 리스트
|
||||||
|
</a>
|
||||||
|
,
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
@click="$router.push('/admin/support/inquiry/list')"
|
||||||
|
>
|
||||||
|
1:1 문의 리스트
|
||||||
|
</a>
|
||||||
|
,
|
||||||
|
|
||||||
|
<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>
|
||||||
816
inspond-nuxt-safekiso/base/pages/admin/user/[uid]/edit.vue
Normal file
816
inspond-nuxt-safekiso/base/pages/admin/user/[uid]/edit.vue
Normal file
@@ -0,0 +1,816 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<div class="divide-y divide-gray-200">
|
||||||
|
<form class="lg:col-span-9" @submit.prevent="doUpdateInfo">
|
||||||
|
<!-- 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">
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
사용자 정보 확인, 변경
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
일부 정보는 다른 사용자들에게 보여질 수 있으니
|
||||||
|
신중하게 입력해 주세요.
|
||||||
|
</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?uid=' + hero
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
단어 통계
|
||||||
|
</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?uid=' + hero
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
사용 통계
|
||||||
|
</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/key/list?uid=' + hero)"
|
||||||
|
>
|
||||||
|
보유 키
|
||||||
|
</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="doHistory"
|
||||||
|
>
|
||||||
|
유저 로그
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div class="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
for="email"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>이메일</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="email"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
name="email"
|
||||||
|
autocomplete="email"
|
||||||
|
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">
|
||||||
|
<label
|
||||||
|
for="email"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>가입일</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="created"
|
||||||
|
v-model="created"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
name="created"
|
||||||
|
autocomplete="created"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-col lg:flex-row">
|
||||||
|
<div class="flex-grow space-y-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="username"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
사용자 이름
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 rounded-md shadow-sm flex">
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="displayName"
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
autocomplete="username"
|
||||||
|
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>
|
||||||
|
<label
|
||||||
|
for="phone"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
전화번호
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 rounded-md shadow-sm flex">
|
||||||
|
<input
|
||||||
|
id="phone"
|
||||||
|
v-model="phone"
|
||||||
|
type="text"
|
||||||
|
name="phone"
|
||||||
|
autocomplete="phone"
|
||||||
|
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>
|
||||||
|
<label
|
||||||
|
for="about"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
간단한 소개
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<textarea
|
||||||
|
id="about"
|
||||||
|
v-model="memo"
|
||||||
|
name="about"
|
||||||
|
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>
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
프로필 사진
|
||||||
|
</label>
|
||||||
|
<div class="mt-2 flex items-center space-x-5">
|
||||||
|
<span
|
||||||
|
v-if="photoUrl == ''"
|
||||||
|
class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-full w-full text-gray-300"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="inline-block h-12 w-12 rounded-full border"
|
||||||
|
:src="photoUrl"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2 sm:col-span-2 pt-3">
|
||||||
|
<div class="mt-1 flex rounded-md shadow-sm">
|
||||||
|
<div
|
||||||
|
class="relative flex items-stretch flex-grow focus-within:z-10"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="photoUrl"
|
||||||
|
v-model="photoUrl"
|
||||||
|
type="text"
|
||||||
|
name="photoUrl"
|
||||||
|
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-none rounded-l-md sm:text-sm border-gray-300"
|
||||||
|
placeholder="http://"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500">
|
||||||
|
프로필로 사용하실 이미지의 주소를
|
||||||
|
입력하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mt-2 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md"
|
||||||
|
@dragover.prevent
|
||||||
|
@dragenter.prevent
|
||||||
|
@drop.prevent="
|
||||||
|
filesChange(
|
||||||
|
'upload-file',
|
||||||
|
$event.dataTransfer.files
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="space-y-1 text-center">
|
||||||
|
<svg
|
||||||
|
class="mx-auto h-12 w-12 text-gray-400"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<div class="flex text-sm text-gray-600">
|
||||||
|
<label
|
||||||
|
for="file-upload"
|
||||||
|
class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
@click="
|
||||||
|
$refs.input_file.click()
|
||||||
|
"
|
||||||
|
>여기</a
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="input_file"
|
||||||
|
type="file"
|
||||||
|
name="upload-file"
|
||||||
|
accept=".jpg,.jpeg,.png"
|
||||||
|
hidden
|
||||||
|
@change="
|
||||||
|
filesChange(
|
||||||
|
$event.target.name,
|
||||||
|
$event.target.files
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p class="pl-1">
|
||||||
|
를 눌러 업로드 하시거나 마우스로
|
||||||
|
이곳에 끌어 놓아 주세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
PNG, JPG 최대 1MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy section -->
|
||||||
|
<div class="pt-0">
|
||||||
|
<div class="mt-4 py-4 px-4 flex justify-end sm:px-6">
|
||||||
|
<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="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="lg:col-span-9" @submit.prevent="doUpdateLevel">
|
||||||
|
<div class="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
자격 변경
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
사용자의 자격을 변경합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<RadioGroup v-model="selectedMailingLists">
|
||||||
|
<div
|
||||||
|
class="mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-4 sm:gap-x-4"
|
||||||
|
>
|
||||||
|
<RadioGroupOption
|
||||||
|
v-for="mailingList in mailingLists"
|
||||||
|
:key="mailingList.level"
|
||||||
|
v-slot="{ checked, active }"
|
||||||
|
as="template"
|
||||||
|
:value="mailingList"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
checked
|
||||||
|
? 'border-transparent'
|
||||||
|
: 'border-gray-300',
|
||||||
|
active
|
||||||
|
? 'border-indigo-500 ring-2 ring-indigo-500'
|
||||||
|
: '',
|
||||||
|
'relative bg-white border rounded-lg shadow-sm p-4 flex cursor-pointer focus:outline-none',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="flex-1 flex">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<RadioGroupLabel
|
||||||
|
as="span"
|
||||||
|
class="block text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{ mailingList.title }}
|
||||||
|
</RadioGroupLabel>
|
||||||
|
<RadioGroupDescription
|
||||||
|
as="span"
|
||||||
|
class="mt-1 flex items-center text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
{{ mailingList.description }}
|
||||||
|
</RadioGroupDescription>
|
||||||
|
<RadioGroupDescription
|
||||||
|
as="span"
|
||||||
|
class="mt-6 text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{ mailingList.users }}
|
||||||
|
</RadioGroupDescription>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<CheckCircleIcon
|
||||||
|
:class="[
|
||||||
|
!checked ? 'invisible' : '',
|
||||||
|
'h-5 w-5 text-indigo-600',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
active ? 'border' : 'border-2',
|
||||||
|
checked
|
||||||
|
? 'border-indigo-500'
|
||||||
|
: 'border-transparent',
|
||||||
|
'absolute -inset-px rounded-lg pointer-events-none',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RadioGroupOption>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy section -->
|
||||||
|
<div class="pt-0">
|
||||||
|
<div class="mt-4 py-4 px-4 flex justify-end sm:px-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
현재 자격 저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="lg:col-span-9" @submit.prevent="doUpdateLimitCount">
|
||||||
|
<div class="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
API 키 갯수 제한
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
쵀대 몇개의 API 키를 생성할 수 있는지를 설정합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div class="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
for="limitCount"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>최대 API 키 갯수 제한</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="limitCount"
|
||||||
|
v-model="limitCount"
|
||||||
|
type="number"
|
||||||
|
name="limitCount"
|
||||||
|
autocomplete="limitCount"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy section -->
|
||||||
|
<div class="pt-0">
|
||||||
|
<div class="mt-4 py-4 px-4 flex justify-end sm:px-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
제한 숫자 저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="lg:col-span-9" @submit.prevent="doUpdatePassword">
|
||||||
|
<div class="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
비밀번호 변경
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
새로운 비밀번호를 두번 정확하게 입력해 주셔야 합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div class="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
for="password"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>새로운 비밀번호</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
autocomplete="password"
|
||||||
|
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">
|
||||||
|
<label
|
||||||
|
for="password2"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>비밀번호 확인</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password2"
|
||||||
|
v-model="password2"
|
||||||
|
type="password"
|
||||||
|
name="password2"
|
||||||
|
autocomplete="password2"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy section -->
|
||||||
|
<div class="pt-0">
|
||||||
|
<div class="mt-4 py-4 px-4 flex justify-end sm:px-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
비밀번호 저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="lg:col-span-9" @submit.prevent="doWithdrawal">
|
||||||
|
<div class="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
회원 탈퇴 처리
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
탈퇴 처리를 하시면 계정은 즉시 탈퇴처리 되며 일정기간
|
||||||
|
동일한 이메일로 재가입 할 수 없습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy section -->
|
||||||
|
<div class="pt-0">
|
||||||
|
<div class="mt-4 py-4 px-4 flex justify-end sm:px-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
탈퇴 처리
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupDescription,
|
||||||
|
RadioGroupLabel,
|
||||||
|
RadioGroupOption,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
import { CheckCircleIcon } from '@heroicons/vue/24/solid/index.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
const mailingLists = [
|
||||||
|
{
|
||||||
|
level: 0,
|
||||||
|
title: '일반 회원',
|
||||||
|
description: '서비스에 가입한 상태이나 아무런 권한이 없습니다.',
|
||||||
|
users: '표기 : user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 5,
|
||||||
|
title: '어드민',
|
||||||
|
description: '모든 기능을 사용할 수 있는 전체 서비스 관리자입니다.',
|
||||||
|
users: '표기 : admin',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mailingLists = [
|
||||||
|
{
|
||||||
|
level: 0,
|
||||||
|
title: '일반 회원',
|
||||||
|
description: '서비스에 가입한 상태이나 아무런 권한이 없습니다.',
|
||||||
|
users: '표기 : user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 3,
|
||||||
|
title: '회원사 운영자',
|
||||||
|
description: '사이트 가입 후 어드민이 승인한 사용자입니다.',
|
||||||
|
users: '표기 : op',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
level: 4,
|
||||||
|
title: '수퍼 운영자',
|
||||||
|
description: '필터 단어 추가 권한이 부여되는 운영자 입니다.',
|
||||||
|
users: '표기 : super',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 5,
|
||||||
|
title: '어드민',
|
||||||
|
description: '모든 기능을 사용할 수 있는 전체 서비스 관리자입니다.',
|
||||||
|
users: '표기 : admin',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedMailingLists = ref(mailingLists[0]);
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const uid = ref('');
|
||||||
|
const email = ref('');
|
||||||
|
const displayName = ref('');
|
||||||
|
const photoUrl = ref('');
|
||||||
|
const phone = ref('');
|
||||||
|
const memo = ref('');
|
||||||
|
|
||||||
|
const created = ref('');
|
||||||
|
|
||||||
|
const password = ref('');
|
||||||
|
const password2 = ref('');
|
||||||
|
|
||||||
|
const limitCount = ref(5);
|
||||||
|
|
||||||
|
// email: '1@1', displayName: '1@1', phone: '', memo: ''
|
||||||
|
|
||||||
|
let userInfo = {};
|
||||||
|
|
||||||
|
function doHistory() {
|
||||||
|
navigateTo(
|
||||||
|
'/admin/user/' + route.params.uid + '/history/' + displayName.value
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
router.push({
|
||||||
|
name: 'admin-user-history',
|
||||||
|
params: { hero: uid.value, name: displayName.value },
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doUpdateLimitCount() {
|
||||||
|
if (isNaN(limitCount.value)) {
|
||||||
|
alert('숫자만 입력할 수 있습니다. ');
|
||||||
|
return false;
|
||||||
|
} else if (
|
||||||
|
limitCount.value < 0 ||
|
||||||
|
limitCount.value > Number.MAX_SAFE_INTEGER
|
||||||
|
) {
|
||||||
|
alert(
|
||||||
|
'입력 가능 범위 내에서 선택해 주세요. 0 ~ ' +
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('update', 'admin:limitCount', {
|
||||||
|
limitCount: limitCount.value,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doUpdateInfo() {
|
||||||
|
const responseJson = await _crossCtl.doComm('update', 'admin:profile', {
|
||||||
|
hero: hero,
|
||||||
|
displayName: displayName.value,
|
||||||
|
photoUrl: photoUrl.value,
|
||||||
|
infos: {
|
||||||
|
email: email.value,
|
||||||
|
phone: phone.value,
|
||||||
|
memo: memo.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doUpdateLevel() {
|
||||||
|
const responseJson = await _crossCtl.doComm('update', 'admin:level', {
|
||||||
|
hero: hero,
|
||||||
|
level: selectedMailingLists.value['level'],
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doUpdatePassword() {
|
||||||
|
if (password.value == '') {
|
||||||
|
alert('변경할 비밀번호는 빈칸이면 안됩니다.');
|
||||||
|
} else if (password.value != password2.value) {
|
||||||
|
alert('변경할 비밀번호가 확인 입력과 일치하지 않습니다.');
|
||||||
|
} else {
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
'update',
|
||||||
|
'admin:password',
|
||||||
|
{
|
||||||
|
hero: hero,
|
||||||
|
password_new: password.value,
|
||||||
|
password_again: password2.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCancel() {
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doWithdrawal() {
|
||||||
|
if (window.confirm('이 회원의 탈퇴 처리를 하시겠습니까?')) {
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
'update',
|
||||||
|
'admin:withdrawal',
|
||||||
|
{
|
||||||
|
hero: hero,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function filesChange(fieldName, fileList) {
|
||||||
|
// handle file changes
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
if (!fileList.length) return;
|
||||||
|
|
||||||
|
// append the files to FormData
|
||||||
|
Array.from(Array(fileList.length).keys()).map((x) => {
|
||||||
|
formData.append(fieldName, fileList[x], fileList[x].name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// save it
|
||||||
|
console.log('formData=', formData);
|
||||||
|
|
||||||
|
formData.append('target', 'just');
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doUpload('just', formData);
|
||||||
|
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
photoUrl.value =
|
||||||
|
_crossCtl.config['API_BASE_URL'].replace('/api/', '') +
|
||||||
|
responseJson['files'][0]['localUrl'];
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hero = route.params.uid as string;
|
||||||
|
|
||||||
|
console.log('hero=', hero);
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('select', 'admin:user:byid', {
|
||||||
|
hero: hero,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
|
||||||
|
const { $customFormat } = useNuxtApp();
|
||||||
|
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
userInfo = responseJson['data'][0];
|
||||||
|
const tmpUserInfo = JSON.parse(userInfo['infos']);
|
||||||
|
|
||||||
|
limitCount.value = userInfo['limit_count'];
|
||||||
|
|
||||||
|
if (tmpUserInfo != null) {
|
||||||
|
uid.value = hero;
|
||||||
|
email.value = tmpUserInfo['email'];
|
||||||
|
displayName.value = userInfo['display_name'];
|
||||||
|
photoUrl.value = userInfo['photo_url'];
|
||||||
|
phone.value = tmpUserInfo['phone'];
|
||||||
|
memo.value = tmpUserInfo['memo'];
|
||||||
|
created.value = $customFormat(userInfo['created']);
|
||||||
|
} else {
|
||||||
|
email.value = 'NaN';
|
||||||
|
displayName.value = userInfo['display_name'];
|
||||||
|
photoUrl.value = userInfo['photo_url'];
|
||||||
|
phone.value = 'NaN';
|
||||||
|
memo.value = 'NaN';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (userInfo['user_level']) {
|
||||||
|
/*
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
selectedMailingLists.value = mailingLists[0];
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
selectedMailingLists.value = mailingLists[0];
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
selectedMailingLists.value = mailingLists[0];
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
selectedMailingLists.value = mailingLists[1];
|
||||||
|
break;
|
||||||
|
|
||||||
|
*/
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
selectedMailingLists.value = mailingLists[0];
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
selectedMailingLists.value = mailingLists[1];
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
selectedMailingLists.value = mailingLists[2];
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
selectedMailingLists.value = mailingLists[3];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('selectedMailingLists.value=', selectedMailingLists.value);
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
<!-- 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()"
|
||||||
|
>←이전 화면으로<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>
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
<!-- 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">
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
로그 상세 보기
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"></p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div class="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
for="email"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>이메일</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="email"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
name="email"
|
||||||
|
autocomplete="email"
|
||||||
|
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="username"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
사용자 이름
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 rounded-md shadow-sm flex">
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="displayName"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
autocomplete="username"
|
||||||
|
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>
|
||||||
|
<label
|
||||||
|
for="phone"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
전화번호
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 rounded-md shadow-sm flex">
|
||||||
|
<input
|
||||||
|
id="phone"
|
||||||
|
v-model="phone"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
name="phone"
|
||||||
|
autocomplete="phone"
|
||||||
|
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>
|
||||||
|
<label
|
||||||
|
for="about"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
간단한 소개
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<textarea
|
||||||
|
id="about"
|
||||||
|
v-model="memo"
|
||||||
|
disabled
|
||||||
|
name="about"
|
||||||
|
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 class="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div class="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
for="email"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>로그 태그</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="logTag"
|
||||||
|
v-model="logTag"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
name="logTag"
|
||||||
|
autocomplete="logTag"
|
||||||
|
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">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="about"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
부가 정보
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<vue-json-pretty
|
||||||
|
class="bg-white shadow-sm focus:ring-sky-500 focus:border-sky-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md"
|
||||||
|
:path="'root'"
|
||||||
|
:data="logMemo"
|
||||||
|
>
|
||||||
|
</vue-json-pretty>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy section -->
|
||||||
|
<div class="pt-0">
|
||||||
|
<div class="mt-4 py-4 px-4 flex justify-end sm:px-6">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const email = ref('');
|
||||||
|
const displayName = ref('');
|
||||||
|
const photoUrl = ref('');
|
||||||
|
const phone = ref('');
|
||||||
|
const memo = ref('');
|
||||||
|
|
||||||
|
const logTag = ref('');
|
||||||
|
const logMemo = ref('');
|
||||||
|
|
||||||
|
let logInfo = {};
|
||||||
|
let userInfo = {};
|
||||||
|
|
||||||
|
async function doCancel() {
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('route.params=', route.params);
|
||||||
|
|
||||||
|
const hero = route.params.hero;
|
||||||
|
const uid = route.params.uid;
|
||||||
|
|
||||||
|
console.log('hero=', hero);
|
||||||
|
|
||||||
|
let responseJson = await _crossCtl.doComm('select', 'admin:user:byid', {
|
||||||
|
hero: uid,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
userInfo = responseJson['data'][0];
|
||||||
|
const tmpUserInfo = _utils.safeJSON(userInfo['infos']);
|
||||||
|
|
||||||
|
if (tmpUserInfo != null) {
|
||||||
|
email.value = tmpUserInfo['email'];
|
||||||
|
displayName.value = userInfo['display_name'];
|
||||||
|
photoUrl.value = userInfo['photo_url'];
|
||||||
|
phone.value = tmpUserInfo['phone'];
|
||||||
|
memo.value = tmpUserInfo['memo'];
|
||||||
|
} else {
|
||||||
|
email.value = 'NaN';
|
||||||
|
displayName.value = userInfo['display_name'];
|
||||||
|
photoUrl.value = userInfo['photo_url'];
|
||||||
|
phone.value = 'NaN';
|
||||||
|
memo.value = 'NaN';
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJson = await _crossCtl.doComm('select', 'log:user', {
|
||||||
|
hero: hero,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
logInfo = responseJson['data'][0];
|
||||||
|
const tmpLogMemo = _utils.safeJSON(logInfo['memo']);
|
||||||
|
|
||||||
|
console.log('logInfo = ', logInfo);
|
||||||
|
console.log('tmpLogMemo = ', tmpLogMemo);
|
||||||
|
|
||||||
|
logTag.value = logInfo['tag'];
|
||||||
|
logMemo.value = tmpLogMemo;
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
156
inspond-nuxt-safekiso/base/pages/admin/user/list.vue
Normal file
156
inspond-nuxt-safekiso/base/pages/admin/user/list.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<!-- 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"></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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const router = useRouter();
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 'domain' },
|
||||||
|
{
|
||||||
|
class: 'mt-1 truncate text-gray-500 sm:hidden',
|
||||||
|
key: 'email',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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: 'domain',
|
||||||
|
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: 'email',
|
||||||
|
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: 'role',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass: 'px-3 py-4 text-sm text-gray-500',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const listActions = ['상세보기'];
|
||||||
|
const actionKey = 'uid';
|
||||||
|
const listKeys = ['serial', 'uid', 'name', 'domain', 'email', 'role'];
|
||||||
|
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);
|
||||||
|
navigateTo('/admin/user/' + 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(
|
||||||
|
'list',
|
||||||
|
'admin:users:level:all',
|
||||||
|
{
|
||||||
|
start: (currentPageNumber.value - 1) * pageSize.value,
|
||||||
|
length: pageSize.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<div class="divide-y divide-gray-200">
|
||||||
|
<form class="lg:col-span-9" @submit.prevent="doUpdate">
|
||||||
|
<!-- 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">
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
가입 허가 항목 수정
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
이미 가입된 사용자에게는 적용되지 않으며, 이 항목이
|
||||||
|
저장된 상태에서 가입시에만 적용됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div class="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
for="uid"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>이메일</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="uid"
|
||||||
|
v-model="uid"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
name="uid"
|
||||||
|
autocomplete="uid"
|
||||||
|
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">
|
||||||
|
<label
|
||||||
|
for="uid"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>생성일</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="created"
|
||||||
|
v-model="created"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
name="created"
|
||||||
|
autocomplete="created"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-col lg:flex-row">
|
||||||
|
<div class="flex-grow space-y-6">
|
||||||
|
<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 class="mt-6 flex flex-col lg:flex-row">
|
||||||
|
<div class="flex-grow space-y-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="level"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
자격 설정
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<RadioGroup v-model="selectedUserLevelInfo">
|
||||||
|
<div
|
||||||
|
class="mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-4 sm:gap-x-4"
|
||||||
|
>
|
||||||
|
<RadioGroupOption
|
||||||
|
v-for="userLevel in userLevels"
|
||||||
|
:key="userLevel.level"
|
||||||
|
v-slot="{ checked, active }"
|
||||||
|
as="template"
|
||||||
|
:value="userLevel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
checked
|
||||||
|
? 'border-transparent'
|
||||||
|
: 'border-gray-300',
|
||||||
|
active
|
||||||
|
? 'border-indigo-500 ring-2 ring-indigo-500'
|
||||||
|
: '',
|
||||||
|
'relative bg-white border rounded-lg shadow-sm p-4 flex cursor-pointer focus:outline-none',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="flex-1 flex">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<RadioGroupLabel
|
||||||
|
as="span"
|
||||||
|
class="block text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
userLevel.title
|
||||||
|
}}
|
||||||
|
</RadioGroupLabel>
|
||||||
|
<RadioGroupDescription
|
||||||
|
as="span"
|
||||||
|
class="mt-1 flex items-center text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
userLevel.description
|
||||||
|
}}
|
||||||
|
</RadioGroupDescription>
|
||||||
|
<RadioGroupDescription
|
||||||
|
as="span"
|
||||||
|
class="mt-6 text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
userLevel.users
|
||||||
|
}}
|
||||||
|
</RadioGroupDescription>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<CheckCircleIcon
|
||||||
|
:class="[
|
||||||
|
!checked
|
||||||
|
? 'invisible'
|
||||||
|
: '',
|
||||||
|
'h-5 w-5 text-indigo-600',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
active
|
||||||
|
? 'border'
|
||||||
|
: 'border-2',
|
||||||
|
checked
|
||||||
|
? 'border-indigo-500'
|
||||||
|
: 'border-transparent',
|
||||||
|
'absolute -inset-px rounded-lg pointer-events-none',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RadioGroupOption>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</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-end sm:px-6">
|
||||||
|
<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-red-700 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="doToggle"
|
||||||
|
>
|
||||||
|
{{ status == 0 ? '삭제' : '복구' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupDescription,
|
||||||
|
RadioGroupLabel,
|
||||||
|
RadioGroupOption,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
import { CheckCircleIcon } from '@heroicons/vue/24/solid/index.js';
|
||||||
|
import { stat } from 'fs/promises';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const userLevels = _crossCtl.siteConfig.userLevels;
|
||||||
|
|
||||||
|
const selectedUserLevelInfo = ref(_crossCtl.siteConfig.userLevels[0]);
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const serial = ref(0);
|
||||||
|
const uid = ref('');
|
||||||
|
const level = ref(0);
|
||||||
|
const memo = ref('');
|
||||||
|
const status = ref(0);
|
||||||
|
const created = ref('');
|
||||||
|
|
||||||
|
// email: '1@1', displayName: '1@1', phone: '', memo: ''
|
||||||
|
|
||||||
|
let userInfo = {};
|
||||||
|
|
||||||
|
function doToggle() {
|
||||||
|
status.value = status.value == 0 ? 4 : 0;
|
||||||
|
doUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doUpdate() {
|
||||||
|
const responseJson = await _crossCtl.doComm('update', 'admin:white', {
|
||||||
|
hero: serial.value,
|
||||||
|
uid: uid.value,
|
||||||
|
level: selectedUserLevelInfo.value['level'],
|
||||||
|
memo: memo.value,
|
||||||
|
status: status.value,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCancel() {
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hero = route.params.hero as string;
|
||||||
|
|
||||||
|
console.log('hero=', hero);
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('select', 'admin:white', {
|
||||||
|
hero: hero,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
|
||||||
|
const { $customFormat } = useNuxtApp();
|
||||||
|
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
userInfo = responseJson['data'][0];
|
||||||
|
|
||||||
|
serial.value = userInfo['serial'];
|
||||||
|
|
||||||
|
uid.value = userInfo['uid'];
|
||||||
|
level.value = userInfo['level'];
|
||||||
|
memo.value = userInfo['memo'];
|
||||||
|
status.value = userInfo['status'];
|
||||||
|
created.value = $customFormat(userInfo['created']);
|
||||||
|
|
||||||
|
for (let i = 0; i < _crossCtl.siteConfig.userLevels.length; i++) {
|
||||||
|
const tmpLevelInfo = _crossCtl.siteConfig.userLevels[i];
|
||||||
|
if (tmpLevelInfo.level == level.value) {
|
||||||
|
selectedUserLevelInfo.value = tmpLevelInfo;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('selectedUserLevelInfo.value=', selectedUserLevelInfo.value);
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
154
inspond-nuxt-safekiso/base/pages/admin/user/white/list.vue
Normal file
154
inspond-nuxt-safekiso/base/pages/admin/user/white/list.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<!-- 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">
|
||||||
|
<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="navigateTo('/admin/user/white/new')"
|
||||||
|
>
|
||||||
|
새 계정 추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BaseTable2
|
||||||
|
: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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const router = useRouter();
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const listHeadings = [
|
||||||
|
{
|
||||||
|
title: '아이디',
|
||||||
|
widthRatio: '100',
|
||||||
|
key: 'uid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '역할',
|
||||||
|
widthRatio: '',
|
||||||
|
key: 'level',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '상태',
|
||||||
|
widthRatio: '',
|
||||||
|
key: 'status',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '생성일',
|
||||||
|
widthRatio: '',
|
||||||
|
key: 'created',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const listActions = ['상세보기'];
|
||||||
|
const actionKey = 'uid';
|
||||||
|
const listKeys = ['serial', 'uid', 'name', 'domain', 'email', 'role'];
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
if (key == 'status') {
|
||||||
|
if (val == 0) {
|
||||||
|
return '정상';
|
||||||
|
} else if (val == 4) {
|
||||||
|
return '삭제';
|
||||||
|
} else {
|
||||||
|
return 'unknown(' + val + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
// return $dayjs(val).format('YY/MM/DD');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == 'level') {
|
||||||
|
switch (val) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
return _crossCtl.siteConfig.userLevelInfo[val]['title'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 'unknown(' + val + ')';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAction(tag, target) {
|
||||||
|
console.log('on doAction(), tag=', tag, ', target=', target);
|
||||||
|
navigateTo('/admin/user/white/edit/' + target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageMove(targetPageIdex) {
|
||||||
|
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
|
||||||
|
currentPageNumber.value = targetPageIdex;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
if (process.client) {
|
||||||
|
const responseJson = await _crossCtl.doComm('list', 'admin:white', {
|
||||||
|
start: (currentPageNumber.value - 1) * pageSize.value,
|
||||||
|
length: pageSize.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
244
inspond-nuxt-safekiso/base/pages/admin/user/white/new.vue
Normal file
244
inspond-nuxt-safekiso/base/pages/admin/user/white/new.vue
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<div class="divide-y divide-gray-200">
|
||||||
|
<form class="lg:col-span-9" @submit.prevent="doInsert">
|
||||||
|
<!-- 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">
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
가입 허가 항목 생성
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
이미 가입된 사용자에게는 적용되지 않으며, 이 항목이
|
||||||
|
저장된 상태 이후 가입시에만 적용됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div class="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
for="uid"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>이메일</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="uid"
|
||||||
|
v-model="uid"
|
||||||
|
type="text"
|
||||||
|
name="uid"
|
||||||
|
autocomplete="uid"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-col lg:flex-row">
|
||||||
|
<div class="flex-grow space-y-6">
|
||||||
|
<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 class="mt-6 flex flex-col lg:flex-row">
|
||||||
|
<div class="flex-grow space-y-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="level"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
자격 설정
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<RadioGroup v-model="selectedUserLevelInfo">
|
||||||
|
<div
|
||||||
|
class="mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-4 sm:gap-x-4"
|
||||||
|
>
|
||||||
|
<RadioGroupOption
|
||||||
|
v-for="userLevel in userLevels"
|
||||||
|
:key="userLevel.level"
|
||||||
|
v-slot="{ checked, active }"
|
||||||
|
as="template"
|
||||||
|
:value="userLevel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
checked
|
||||||
|
? 'border-transparent'
|
||||||
|
: 'border-gray-300',
|
||||||
|
active
|
||||||
|
? 'border-indigo-500 ring-2 ring-indigo-500'
|
||||||
|
: '',
|
||||||
|
'relative bg-white border rounded-lg shadow-sm p-4 flex cursor-pointer focus:outline-none',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="flex-1 flex">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<RadioGroupLabel
|
||||||
|
as="span"
|
||||||
|
class="block text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
userLevel.title
|
||||||
|
}}
|
||||||
|
</RadioGroupLabel>
|
||||||
|
<RadioGroupDescription
|
||||||
|
as="span"
|
||||||
|
class="mt-1 flex items-center text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
userLevel.description
|
||||||
|
}}
|
||||||
|
</RadioGroupDescription>
|
||||||
|
<RadioGroupDescription
|
||||||
|
as="span"
|
||||||
|
class="mt-6 text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
userLevel.users
|
||||||
|
}}
|
||||||
|
</RadioGroupDescription>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<CheckCircleIcon
|
||||||
|
:class="[
|
||||||
|
!checked
|
||||||
|
? 'invisible'
|
||||||
|
: '',
|
||||||
|
'h-5 w-5 text-indigo-600',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
active
|
||||||
|
? 'border'
|
||||||
|
: 'border-2',
|
||||||
|
checked
|
||||||
|
? 'border-indigo-500'
|
||||||
|
: 'border-transparent',
|
||||||
|
'absolute -inset-px rounded-lg pointer-events-none',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RadioGroupOption>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</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-end sm:px-6">
|
||||||
|
<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="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupDescription,
|
||||||
|
RadioGroupLabel,
|
||||||
|
RadioGroupOption,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
import { CheckCircleIcon } from '@heroicons/vue/24/solid/index.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const userLevels = _crossCtl.siteConfig.userLevels;
|
||||||
|
|
||||||
|
const selectedUserLevelInfo = ref(_crossCtl.siteConfig.userLevels[0]);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const uid = ref('');
|
||||||
|
const level = ref(3);
|
||||||
|
const memo = ref('');
|
||||||
|
|
||||||
|
// email: '1@1', displayName: '1@1', phone: '', memo: ''
|
||||||
|
|
||||||
|
for (let i = 0; i < _crossCtl.siteConfig.userLevels.length; i++) {
|
||||||
|
const tmpLevelInfo = _crossCtl.siteConfig.userLevels[i];
|
||||||
|
if (tmpLevelInfo.level == level.value) {
|
||||||
|
selectedUserLevelInfo.value = tmpLevelInfo;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doInsert() {
|
||||||
|
const tmpUID = uid.value;
|
||||||
|
|
||||||
|
console.log('tmpUID = ', tmpUID);
|
||||||
|
|
||||||
|
if (tmpUID.trim() == '') {
|
||||||
|
alert('이메일 주소를 아이디로 입력해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('insert', 'admin:white', {
|
||||||
|
uid: uid.value,
|
||||||
|
level: selectedUserLevelInfo.value['level'],
|
||||||
|
memo: memo.value,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
if (responseJson['responseMessage'].startsWith('ER_DUP_ENTRY:')) {
|
||||||
|
alert('이미 동일 아이디가 설정되어 있습니다.');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCancel() {
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Page head goes here -->
|
||||||
|
<div class="px-4 py-5 sm:px-6">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center flex-wrap sm:flex-nowrap"
|
||||||
|
>
|
||||||
|
<div class="">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ pageTitle }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
{{ pageDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<button
|
||||||
|
v-for="(headingAction, index) in headingActions"
|
||||||
|
:key="headingAction"
|
||||||
|
type="button"
|
||||||
|
:class="index > 0 ? 'ml-3' : ''"
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
@click="doHeadingAction(headingAction)"
|
||||||
|
>
|
||||||
|
{{ headingAction }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-4 lg:px-4">
|
||||||
|
<!-- Content goes here -->
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
v-model="title"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
placeholder="제목을 입력하세요."
|
||||||
|
class="mb-3 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
<quill-editor
|
||||||
|
v-model:content="content"
|
||||||
|
class="min-h-[30rem]"
|
||||||
|
:modules="modules"
|
||||||
|
:toolbar="toolbarOptions"
|
||||||
|
placeholder="본문을 입력하세요."
|
||||||
|
@ready="onEditorReady($event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<base-attachment-ctl1
|
||||||
|
v-if="attachmentEnabled"
|
||||||
|
:attachments="attachments"
|
||||||
|
:read-only-flag="false"
|
||||||
|
:update-attachments="updateAttachments"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-5 flex justify-between items-center flex-wrap">
|
||||||
|
<div class="ml-4 mt-4"></div>
|
||||||
|
<div class="ml-4 mt-4 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center rounded-md border border-transparent bg-red-600 px-3 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
||||||
|
@click="doFooterAction('취소')"
|
||||||
|
>
|
||||||
|
{{ '취소' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ml-3 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="doFooterAction('저장')"
|
||||||
|
>
|
||||||
|
{{ '저장' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import BlotFormatter from 'quill-blot-formatter/dist/BlotFormatter';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
// middleware: 'check-auth-admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const bid = ref('');
|
||||||
|
let cid: string | string[] = '';
|
||||||
|
|
||||||
|
bid.value = route.params.boardId[0];
|
||||||
|
cid = route.params['_cid'];
|
||||||
|
|
||||||
|
const modules = {
|
||||||
|
name: 'blotFormatter',
|
||||||
|
module: BlotFormatter,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageTitle = cid == undefined ? '광장 - 새글 작성' : '광장 - 글 수정';
|
||||||
|
const pageDescription =
|
||||||
|
cid == undefined ? '새로운 글을 작성 합니다.' : '내 글을 수정합니다.';
|
||||||
|
|
||||||
|
// 해당 페이지 우측 상단에 표시될 액션 버튼들
|
||||||
|
const headingActions = [];
|
||||||
|
|
||||||
|
const title = ref('');
|
||||||
|
const content = ref({ ops: [] });
|
||||||
|
const attachments = ref([]);
|
||||||
|
const status = ref(0);
|
||||||
|
|
||||||
|
function updateAttachments(newAttachments) {
|
||||||
|
console.log('newAttachments=', newAttachments);
|
||||||
|
attachments.value = newAttachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolbarOptions = [
|
||||||
|
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
||||||
|
['blockquote', 'code-block'],
|
||||||
|
|
||||||
|
// [{ header: 1 }, { header: 2 }], // custom button values
|
||||||
|
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||||
|
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
|
||||||
|
[{ indent: '-1' }, { indent: '+1' }], // outdent/indent
|
||||||
|
// [{ direction: 'rtl' }], // text direction
|
||||||
|
|
||||||
|
// [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
|
||||||
|
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||||
|
|
||||||
|
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
|
||||||
|
[{ font: [] }],
|
||||||
|
[{ align: [] }],
|
||||||
|
['link', 'video', 'image'],
|
||||||
|
['clean'], // remove formatting button
|
||||||
|
];
|
||||||
|
|
||||||
|
let quill = null;
|
||||||
|
|
||||||
|
function selectLocalImage() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.setAttribute('type', 'file');
|
||||||
|
input.click();
|
||||||
|
|
||||||
|
// Listen upload local image and save to server
|
||||||
|
input.onchange = () => {
|
||||||
|
const file = input.files[0];
|
||||||
|
|
||||||
|
// file type is only image.
|
||||||
|
if (/^image\//.test(file.type)) {
|
||||||
|
console.warn('upload images...');
|
||||||
|
saveToServer(file);
|
||||||
|
} else {
|
||||||
|
console.warn('You could only upload images.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLocalFiles() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.setAttribute('type', 'file');
|
||||||
|
input.click();
|
||||||
|
|
||||||
|
// Listen upload local image and save to server
|
||||||
|
input.onchange = () => {
|
||||||
|
console.log('we got file(s) : ', input.files);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveToServer(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('upload-file', file, file.name);
|
||||||
|
// save it
|
||||||
|
|
||||||
|
formData.append('target', 'just');
|
||||||
|
|
||||||
|
console.log('formData=', formData);
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doUpload('just', formData);
|
||||||
|
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
insertToEditor(
|
||||||
|
_crossCtl.config['API_BASE_URL'].replace('/api/', '') +
|
||||||
|
responseJson['files'][0]['localUrl']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function insertToEditor(url: string) {
|
||||||
|
// push image url to rich editor.
|
||||||
|
|
||||||
|
const range = quill.getSelection();
|
||||||
|
quill.insertEmbed(range.index, 'image', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditorReady(e) {
|
||||||
|
console.log('onEditorReady() e = ', e);
|
||||||
|
|
||||||
|
quill = e;
|
||||||
|
|
||||||
|
// quill editor add image handler
|
||||||
|
|
||||||
|
e.getModule('toolbar').addHandler('image', () => {
|
||||||
|
selectLocalImage();
|
||||||
|
});
|
||||||
|
|
||||||
|
e.getModule('toolbar').addHandler('attachment', () => {
|
||||||
|
selectLocalFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cid != undefined) {
|
||||||
|
loadContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachmentEnabled = ref(false);
|
||||||
|
|
||||||
|
async function loadContent() {
|
||||||
|
const responseJson = await _crossCtl.doComm('select', 'board', {
|
||||||
|
boardId: bid.value,
|
||||||
|
hero: cid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
console.log('huk responseJson=', responseJson);
|
||||||
|
const tmpDatas = responseJson['data'];
|
||||||
|
console.log('huk tmpDatas=', tmpDatas);
|
||||||
|
if (tmpDatas.length == 1) {
|
||||||
|
attachmentEnabled.value =
|
||||||
|
responseJson['metaData']['attachmentEnabled'];
|
||||||
|
|
||||||
|
title.value = tmpDatas[0]['title'];
|
||||||
|
|
||||||
|
// content.value = tmpDatas[0]['content'];
|
||||||
|
quill.root.innerHTML = tmpDatas[0]['content'];
|
||||||
|
|
||||||
|
attachments.value = JSON.parse(tmpDatas[0]['attachments']);
|
||||||
|
status.value = tmpDatas[0]['status'];
|
||||||
|
|
||||||
|
// $dayjs(val).format('YY/MM/DD A h:mm:ss')
|
||||||
|
} else {
|
||||||
|
alert('bad count. count = ' + tmpDatas.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
const attachments = [
|
||||||
|
{ name: 'resume_front_end_developer.pdf', href: '#' },
|
||||||
|
{ name: 'coverletter_front_end_developer.pdf', href: '#' },
|
||||||
|
];
|
||||||
|
*/
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function doHeadingAction(tag) {
|
||||||
|
console.log('on doHeadingAction(), tag=', tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doFooterAction(tag) {
|
||||||
|
console.log('on doFooterAction(), tag=', tag);
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
case '저장':
|
||||||
|
// console.log('quill.root.innerHTML=', quill.root.innerHTML);
|
||||||
|
// console.log('content=', content.value);
|
||||||
|
updateContent();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '취소':
|
||||||
|
// router.back();
|
||||||
|
navigateTo('/board/' + bid.value + '/list');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateContent() {
|
||||||
|
if (title.value.trim() == '') {
|
||||||
|
alert('제목을 입력해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let emptyContent = true;
|
||||||
|
|
||||||
|
console.log('content.value = ', content.value);
|
||||||
|
console.log('quill.root.innerHTML = ', quill.root.innerHTML);
|
||||||
|
|
||||||
|
for (let i = 0; i < content.value['ops'].length; i++) {
|
||||||
|
console.log("content.value['ops'][i] = ", content.value['ops'][i]);
|
||||||
|
|
||||||
|
if (typeof content.value['ops'][i]['insert'] == 'string') {
|
||||||
|
if (content.value['ops'][i]['insert'].trim() != '') {
|
||||||
|
emptyContent = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyContent = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (emptyContent == true) {
|
||||||
|
alert('본문을 입력해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm(
|
||||||
|
cid == undefined ? 'insert' : 'update',
|
||||||
|
'board',
|
||||||
|
{
|
||||||
|
boardId: bid.value,
|
||||||
|
hero: cid,
|
||||||
|
title: title.value,
|
||||||
|
content: quill.root.innerHTML,
|
||||||
|
attachments: attachments.value,
|
||||||
|
status: status.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
// router.back();
|
||||||
|
navigateTo('/board/' + bid.value + '/list');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh();
|
||||||
|
</script>
|
||||||
216
inspond-nuxt-safekiso/base/pages/board/[...boardId]/list.vue
Normal file
216
inspond-nuxt-safekiso/base/pages/board/[...boardId]/list.vue
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Page head goes here -->
|
||||||
|
<div class="px-3 py-5">
|
||||||
|
<div
|
||||||
|
class="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"
|
||||||
|
>
|
||||||
|
<div class="ml-4 mt-4">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ pageTitle }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
{{ pageDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4 mt-4 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
v-for="(headingAction, index) in headingActions"
|
||||||
|
:key="headingAction"
|
||||||
|
type="button"
|
||||||
|
:class="index > 0 ? 'ml-3' : ''"
|
||||||
|
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="doHeadingAction(headingAction)"
|
||||||
|
>
|
||||||
|
{{ headingAction }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-w mx-auto px-3">
|
||||||
|
<!-- Content goes here -->
|
||||||
|
|
||||||
|
<BaseBoardList1
|
||||||
|
:headings="listHeadings"
|
||||||
|
:actions="listActions"
|
||||||
|
: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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
// middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageTitle = ref('');
|
||||||
|
const pageDescription = ref('');
|
||||||
|
|
||||||
|
// 해당 페이지 우측 상단에 표시될 액션 버튼들
|
||||||
|
const headingActions = ['글쓰기'];
|
||||||
|
|
||||||
|
// 리스트 쓰는 경에만 해당. 안되는 경우 모두 지울것.
|
||||||
|
const listSource = 'list';
|
||||||
|
const listTarget = 'board';
|
||||||
|
|
||||||
|
const listActions = [];
|
||||||
|
const actionKey = 'cid';
|
||||||
|
|
||||||
|
const listHeadings = [
|
||||||
|
{
|
||||||
|
title: '제목',
|
||||||
|
widthRatio: '100',
|
||||||
|
key: 'title',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '글쓴이',
|
||||||
|
widthRatio: '0',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '날짜',
|
||||||
|
widthRatio: '0',
|
||||||
|
key: 'created',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '조회',
|
||||||
|
widthRatio: '0',
|
||||||
|
key: 'hit_count',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '댓글',
|
||||||
|
widthRatio: '0',
|
||||||
|
key: 'comment_count',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const listData = ref([]);
|
||||||
|
|
||||||
|
const boardMeta = ref({});
|
||||||
|
|
||||||
|
const totalPageCount = ref(1);
|
||||||
|
const route = useRoute();
|
||||||
|
const currentPageNumber = ref(route.query.page ? Number(route.query.page) : 1);
|
||||||
|
const pageSize = ref(3);
|
||||||
|
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') {
|
||||||
|
if (boardMeta.value['ago_enabled'] == 1) {
|
||||||
|
return $dayjs(val).fromNow();
|
||||||
|
} else {
|
||||||
|
return $dayjs(val).format('YYYY/MM/DD A h:mm:ss');
|
||||||
|
}
|
||||||
|
|
||||||
|
// return $dayjs(val).format('YY/MM/DD');
|
||||||
|
} else {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function doHeadingAction(tag) {
|
||||||
|
// console.log('on doHeadingAction(), tag=', tag);
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
case '글쓰기':
|
||||||
|
navigateTo('/board/' + currnetBoardId.value + '/new');
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// alert('headingAction : ' + tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAction(tag, target) {
|
||||||
|
console.log('on doAction(), tag=', tag, ', target=', target);
|
||||||
|
|
||||||
|
// alert('doAction : ' + tag + ', target = ' + target);
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
case '보기':
|
||||||
|
navigateTo('/board/' + currnetBoardId.value + '/view/' + target);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageMove(targetPageIdex) {
|
||||||
|
// console.log('on pageMove(), targetPageIdex=', targetPageIdex);
|
||||||
|
currentPageNumber.value = targetPageIdex;
|
||||||
|
navigateTo(
|
||||||
|
'/board/' + currnetBoardId.value + '/list?page=' + targetPageIdex
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
const responseJson = await _crossCtl.doComm(listSource, listTarget, {
|
||||||
|
hero: currnetBoardId.value,
|
||||||
|
start: (currentPageNumber.value - 1) * pageSize.value,
|
||||||
|
length: pageSize.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
if (responseJson['responseMessage'] == 'Unauthorized') {
|
||||||
|
alert(
|
||||||
|
'이 게시판을 볼 수 있는 권한이 없습니다. 확인을 누르면 서비스 메인 화면으로 이동합니다. '
|
||||||
|
);
|
||||||
|
navigateTo('/', { replace: true });
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
boardMeta.value = responseJson['metaData'];
|
||||||
|
pageTitle.value = responseJson['metaData']['title'];
|
||||||
|
pageDescription.value = responseJson['metaData']['description'];
|
||||||
|
|
||||||
|
currentPageNumber.value = responseJson['currentPageNumber'];
|
||||||
|
totalPageCount.value = responseJson['totalPageCount'];
|
||||||
|
pageSize.value = parseInt(responseJson['pageSize']);
|
||||||
|
recordsTotal.value = responseJson['recordsTotal'];
|
||||||
|
|
||||||
|
listData.value = responseJson['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh();
|
||||||
|
|
||||||
|
console.log('huk params = ', route.params);
|
||||||
|
|
||||||
|
const currnetBoardId = ref('');
|
||||||
|
|
||||||
|
if (route.params.boardId instanceof Array) {
|
||||||
|
if (route.params.boardId.length != 1) {
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
console.log('huk 3');
|
||||||
|
currnetBoardId.value = route.params.boardId[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throwError('$404');
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
</script>
|
||||||
310
inspond-nuxt-safekiso/base/pages/board/[...boardId]/new.vue
Normal file
310
inspond-nuxt-safekiso/base/pages/board/[...boardId]/new.vue
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Page head goes here -->
|
||||||
|
<div class="px-4 py-5 sm:px-6">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center flex-wrap sm:flex-nowrap"
|
||||||
|
>
|
||||||
|
<div class="">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ pageTitle }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
{{ pageDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<button
|
||||||
|
v-for="(headingAction, index) in headingActions"
|
||||||
|
:key="headingAction"
|
||||||
|
type="button"
|
||||||
|
:class="index > 0 ? 'ml-3' : ''"
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
@click="doHeadingAction(headingAction)"
|
||||||
|
>
|
||||||
|
{{ headingAction }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-4 lg:px-4">
|
||||||
|
<!-- Content goes here -->
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
v-model="title"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
placeholder="제목을 입력하세요."
|
||||||
|
class="mb-3 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
<quill-editor
|
||||||
|
v-model:content="content"
|
||||||
|
class="min-h-[30rem]"
|
||||||
|
:modules="modules"
|
||||||
|
:toolbar="toolbarOptions"
|
||||||
|
placeholder="본문을 입력하세요."
|
||||||
|
@ready="onEditorReady($event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<base-attachment-ctl1
|
||||||
|
v-if="attachmentEnabled"
|
||||||
|
:attachments="attachments"
|
||||||
|
:read-only-flag="false"
|
||||||
|
:update-attachments="updateAttachments"
|
||||||
|
:board-id="bid"
|
||||||
|
:secure-enabled="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-5 flex justify-between items-center flex-wrap">
|
||||||
|
<div class="ml-4 mt-4"></div>
|
||||||
|
<div class="ml-4 mt-4 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mr-3 inline-flex items-center rounded-md border border-transparent bg-red-600 px-3 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
||||||
|
@click="doFooterAction('취소')"
|
||||||
|
>
|
||||||
|
{{ '취소' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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="doFooterAction('저장')"
|
||||||
|
>
|
||||||
|
{{ '저장' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import BlotFormatter from 'quill-blot-formatter/dist/BlotFormatter';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
// middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
console.log('huk params = ', route.params);
|
||||||
|
|
||||||
|
const bid = ref('');
|
||||||
|
|
||||||
|
if (route.params.boardId instanceof Array) {
|
||||||
|
if (route.params.boardId.length != 1) {
|
||||||
|
throwError('$404');
|
||||||
|
} else {
|
||||||
|
console.log('huk 3');
|
||||||
|
bid.value = route.params.boardId[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throwError('$404');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('huk bid = ', bid.value);
|
||||||
|
|
||||||
|
const modules = {
|
||||||
|
name: 'blotFormatter',
|
||||||
|
module: BlotFormatter,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('select', 'board:info', {
|
||||||
|
hero: bid.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('responseJson = ', responseJson);
|
||||||
|
|
||||||
|
const boardInfo = ref({});
|
||||||
|
|
||||||
|
const attachmentEnabled = ref(false);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
if (responseJson['data'].length > 0) {
|
||||||
|
boardInfo.value = responseJson['data'][0];
|
||||||
|
|
||||||
|
attachmentEnabled.value =
|
||||||
|
responseJson['data'][0]['attachment_enabled'] == 1;
|
||||||
|
|
||||||
|
console.log('attachmentEnabled.value=', attachmentEnabled.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageTitle = boardInfo.value['title'] + ' - ' + '새글 작성';
|
||||||
|
const pageDescription = '새로운 글을 작성 합니다.';
|
||||||
|
|
||||||
|
// 해당 페이지 우측 상단에 표시될 액션 버튼들
|
||||||
|
const headingActions = [];
|
||||||
|
|
||||||
|
const title = ref('');
|
||||||
|
const content = ref({ ops: [] });
|
||||||
|
const attachments = ref([]);
|
||||||
|
const status = ref(0);
|
||||||
|
|
||||||
|
function updateAttachments(newAttachments) {
|
||||||
|
console.log('newAttachments=', newAttachments);
|
||||||
|
attachments.value = newAttachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolbarOptions = [
|
||||||
|
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
||||||
|
['blockquote', 'code-block'],
|
||||||
|
|
||||||
|
// [{ header: 1 }, { header: 2 }], // custom button values
|
||||||
|
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||||
|
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
|
||||||
|
[{ indent: '-1' }, { indent: '+1' }], // outdent/indent
|
||||||
|
// [{ direction: 'rtl' }], // text direction
|
||||||
|
|
||||||
|
// [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
|
||||||
|
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||||
|
|
||||||
|
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
|
||||||
|
[{ font: [] }],
|
||||||
|
[{ align: [] }],
|
||||||
|
['link', 'video', 'image'],
|
||||||
|
['clean'], // remove formatting button
|
||||||
|
];
|
||||||
|
|
||||||
|
let quill = null;
|
||||||
|
|
||||||
|
function selectLocalImage() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.setAttribute('type', 'file');
|
||||||
|
input.click();
|
||||||
|
|
||||||
|
// Listen upload local image and save to server
|
||||||
|
input.onchange = () => {
|
||||||
|
const file = input.files[0];
|
||||||
|
|
||||||
|
// file type is only image.
|
||||||
|
if (/^image\//.test(file.type)) {
|
||||||
|
console.warn('upload images...');
|
||||||
|
saveToServer(file);
|
||||||
|
} else {
|
||||||
|
console.warn('You could only upload images.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLocalFiles() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.setAttribute('type', 'file');
|
||||||
|
input.click();
|
||||||
|
|
||||||
|
// Listen upload local image and save to server
|
||||||
|
input.onchange = () => {
|
||||||
|
console.log('we got file(s) : ', input.files);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveToServer(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('upload-file', file, file.name);
|
||||||
|
// save it
|
||||||
|
|
||||||
|
formData.append('target', 'just');
|
||||||
|
formData.append('attachedTo', bid.value);
|
||||||
|
|
||||||
|
console.log('formData=', formData);
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doUpload('just', formData);
|
||||||
|
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
insertToEditor(
|
||||||
|
// _crossCtl.config['API_BASE_URL'].replace('/api/', '') +
|
||||||
|
responseJson['files'][0]['localUrl']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function insertToEditor(url: string) {
|
||||||
|
// push image url to rich editor.
|
||||||
|
|
||||||
|
const range = quill.getSelection();
|
||||||
|
quill.insertEmbed(range.index, 'image', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditorReady(e) {
|
||||||
|
console.log('onEditorReady() e = ', e);
|
||||||
|
|
||||||
|
quill = e;
|
||||||
|
|
||||||
|
// quill editor add image handler
|
||||||
|
|
||||||
|
e.getModule('toolbar').addHandler('image', () => {
|
||||||
|
selectLocalImage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function doFooterAction(tag) {
|
||||||
|
console.log('on doFooterAction(), tag=', tag);
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
case '저장':
|
||||||
|
// console.log('quill.root.innerHTML=', quill.root.innerHTML);
|
||||||
|
// console.log('content=', content.value);
|
||||||
|
updateContent();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '취소':
|
||||||
|
// router.back();
|
||||||
|
navigateTo('/board/' + bid.value + '/list', { replace: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateContent() {
|
||||||
|
if (title.value.trim() == '') {
|
||||||
|
alert('제목을 입력해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let emptyContent = true;
|
||||||
|
|
||||||
|
console.log('content.value = ', content.value);
|
||||||
|
console.log('quill.root.innerHTML = ', quill.root.innerHTML);
|
||||||
|
|
||||||
|
for (let i = 0; i < content.value['ops'].length; i++) {
|
||||||
|
console.log("content.value['ops'][i] = ", content.value['ops'][i]);
|
||||||
|
|
||||||
|
if (typeof content.value['ops'][i]['insert'] == 'string') {
|
||||||
|
if (content.value['ops'][i]['insert'].trim() != '') {
|
||||||
|
emptyContent = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyContent = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (emptyContent == true) {
|
||||||
|
alert('본문을 입력해 주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('insert', 'board', {
|
||||||
|
boardId: bid.value,
|
||||||
|
title: title.value,
|
||||||
|
content: quill.root.innerHTML,
|
||||||
|
attachments: attachments.value,
|
||||||
|
status: status.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
// router.back();
|
||||||
|
navigateTo('/board/' + bid.value + '/list', { replace: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Page head goes here -->
|
||||||
|
<div class="max-w mx-auto px-3">
|
||||||
|
<!-- Content goes here -->
|
||||||
|
<BaseBoardView1
|
||||||
|
:name="name"
|
||||||
|
:profile-url="profileUrl"
|
||||||
|
:title="title"
|
||||||
|
:content="content"
|
||||||
|
:flags="flags"
|
||||||
|
:attachments="attachments"
|
||||||
|
:hit-count="hitCount"
|
||||||
|
:like-count="likeCount"
|
||||||
|
:dislike-count="dislikeCount"
|
||||||
|
:comment-count="commentCount"
|
||||||
|
:report-count="reportCount"
|
||||||
|
:status="status"
|
||||||
|
:updated="updated"
|
||||||
|
:created="created"
|
||||||
|
/>
|
||||||
|
<BaseAttachmentCtl1
|
||||||
|
v-if="attachmentEnabled"
|
||||||
|
:attachments="attachments"
|
||||||
|
:read-only-flag="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-5 flex justify-between items-center flex-wrap">
|
||||||
|
<div class="ml-4 mt-4"></div>
|
||||||
|
<div class="ml-4 mt-4 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mr-3 inline-flex items-center rounded-md border border-transparent bg-indigo-100 px-3 py-2 text-sm font-medium leading-4 text-indigo-700 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
@click="doFooterAction('닫기')"
|
||||||
|
>
|
||||||
|
{{ '닫기' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="myFlag"
|
||||||
|
type="button"
|
||||||
|
class="mr-3 inline-flex items-center rounded-md border border-transparent bg-red-600 px-3 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
||||||
|
@click="doFooterAction('삭제')"
|
||||||
|
>
|
||||||
|
{{ '삭제' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="myFlag"
|
||||||
|
type="button"
|
||||||
|
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="doFooterAction('수정')"
|
||||||
|
>
|
||||||
|
{{ '수정' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BaseCommentCtl1
|
||||||
|
v-if="commentEnabled"
|
||||||
|
class="mt-5"
|
||||||
|
:tid="commentTargetId"
|
||||||
|
:read-only-flag="!_crossCtl.isAuthenticated"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
// middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
console.log('huk params = ', route.params);
|
||||||
|
|
||||||
|
const bid = ref('');
|
||||||
|
let cid: string | string[] = '';
|
||||||
|
|
||||||
|
bid.value = route.params.boardId[0];
|
||||||
|
cid = route.params['_cid'];
|
||||||
|
|
||||||
|
console.log('huk 7');
|
||||||
|
|
||||||
|
const commentTargetId = ref(cid.toString());
|
||||||
|
|
||||||
|
const name = ref('');
|
||||||
|
const profileUrl = ref('');
|
||||||
|
const title = ref('');
|
||||||
|
const content = ref('');
|
||||||
|
const flags = ref([]);
|
||||||
|
const attachments = ref([]);
|
||||||
|
|
||||||
|
const hitCount = ref(0);
|
||||||
|
const likeCount = ref(0);
|
||||||
|
const dislikeCount = ref(0);
|
||||||
|
const commentCount = ref(0);
|
||||||
|
const reportCount = ref(0);
|
||||||
|
const status = ref(0);
|
||||||
|
const updated = ref('');
|
||||||
|
const created = ref('');
|
||||||
|
|
||||||
|
const myFlag = ref(false);
|
||||||
|
|
||||||
|
const commentEnabled = ref(false);
|
||||||
|
const attachmentEnabled = ref(false);
|
||||||
|
|
||||||
|
const { $dayjs } = useNuxtApp();
|
||||||
|
|
||||||
|
function doFooterAction(tag) {
|
||||||
|
console.log('on doFooterAction(), tag=', tag);
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
case '수정':
|
||||||
|
_crossCtl.openModal(
|
||||||
|
'confirm',
|
||||||
|
'수정 확인',
|
||||||
|
'정말로 수정하시겠습니까?',
|
||||||
|
['수정', '취소'],
|
||||||
|
(serial, btnIdx) => {
|
||||||
|
console.log('btnIdx=', btnIdx);
|
||||||
|
if (btnIdx == 0) {
|
||||||
|
navigateTo('/board/' + bid.value + '/edit/' + cid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '삭제':
|
||||||
|
doDelete();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '닫기':
|
||||||
|
// router.back();
|
||||||
|
navigateTo('/board/' + bid.value + '/list', { replace: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doDelete() {
|
||||||
|
_crossCtl.openModal(
|
||||||
|
'confirm',
|
||||||
|
'삭제 확인',
|
||||||
|
'정말로 삭제하시겠습니까?',
|
||||||
|
['삭제', '취소'],
|
||||||
|
async (serial, btnIdx) => {
|
||||||
|
console.log('btnIdx=', btnIdx);
|
||||||
|
if (btnIdx == 0) {
|
||||||
|
const responseJson = await _crossCtl.doComm('delete', 'board', {
|
||||||
|
boardId: bid.value,
|
||||||
|
hero: cid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
// router.back();
|
||||||
|
navigateTo('/board/' + bid.value + '/list', {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const responseJson = await _crossCtl.doComm('select', 'board', {
|
||||||
|
boardId: bid.value,
|
||||||
|
hero: cid,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('huk responseJson=', responseJson);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
const tmpDatas = responseJson['data'];
|
||||||
|
console.log('huk tmpDatas=', tmpDatas);
|
||||||
|
if (tmpDatas.length == 1) {
|
||||||
|
commentEnabled.value = responseJson['metaData']['commentEnabled'];
|
||||||
|
attachmentEnabled.value = responseJson['metaData']['attachmentEnabled'];
|
||||||
|
name.value = tmpDatas[0]['name'];
|
||||||
|
profileUrl.value = tmpDatas[0]['profile_url'];
|
||||||
|
title.value = tmpDatas[0]['title'];
|
||||||
|
content.value = tmpDatas[0]['content'];
|
||||||
|
flags.value = JSON.parse(tmpDatas[0]['flags']);
|
||||||
|
attachments.value = JSON.parse(tmpDatas[0]['attachments']);
|
||||||
|
hitCount.value = tmpDatas[0]['hit_count'];
|
||||||
|
likeCount.value = tmpDatas[0]['like_count'];
|
||||||
|
dislikeCount.value = tmpDatas[0]['dislike_count'];
|
||||||
|
commentCount.value = tmpDatas[0]['comment_count'];
|
||||||
|
reportCount.value = tmpDatas[0]['report_count'];
|
||||||
|
status.value = tmpDatas[0]['status'];
|
||||||
|
updated.value = $dayjs(tmpDatas[0]['updated']).format(
|
||||||
|
'YY/MM/DD A h:mm:ss'
|
||||||
|
);
|
||||||
|
created.value = $dayjs(tmpDatas[0]['created']).format(
|
||||||
|
'YY/MM/DD A h:mm:ss'
|
||||||
|
);
|
||||||
|
|
||||||
|
myFlag.value = tmpDatas[0]['myFlag'];
|
||||||
|
|
||||||
|
// $dayjs(val).format('YY/MM/DD A h:mm:ss')
|
||||||
|
} else {
|
||||||
|
console.log('bad count. count = ' + tmpDatas.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
47
inspond-nuxt-safekiso/base/pages/doc/[...target].vue
Normal file
47
inspond-nuxt-safekiso/base/pages/doc/[...target].vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="prose p-0 max-w-none">
|
||||||
|
<ContentRenderer :value="targetData" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute();
|
||||||
|
let targetData = ref();
|
||||||
|
let target = '';
|
||||||
|
|
||||||
|
const { path } = useRoute();
|
||||||
|
|
||||||
|
if (route.params.target instanceof Array) {
|
||||||
|
switch (route.params.target[0]) {
|
||||||
|
case 'kss':
|
||||||
|
case 'api_doc':
|
||||||
|
case 'bill':
|
||||||
|
case 'guide':
|
||||||
|
// case 'privacy':
|
||||||
|
// case 'stipulation':
|
||||||
|
case 'contract':
|
||||||
|
|
||||||
|
case 'certification':
|
||||||
|
case 'manual':
|
||||||
|
target = route.params.target[0];
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target != '') {
|
||||||
|
const { data } = await useAsyncData(`content-${path}`, () => {
|
||||||
|
return queryContent().where({ _path: path }).findOne();
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
const { data } = await useAsyncData('home', () =>
|
||||||
|
queryContent('/doc/' + target).findOne()
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
targetData = data;
|
||||||
|
} else {
|
||||||
|
throwError('$404');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throwError('$404');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
60
inspond-nuxt-safekiso/base/pages/maps/index.vue
Normal file
60
inspond-nuxt-safekiso/base/pages/maps/index.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mt-5 sm:px-3 lg:px-5">구글 맵</div>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<GMapMap
|
||||||
|
:center="center"
|
||||||
|
:zoom="15"
|
||||||
|
:options="{
|
||||||
|
zoomControl: true,
|
||||||
|
mapTypeControl: false,
|
||||||
|
scaleControl: false,
|
||||||
|
streetViewControl: false,
|
||||||
|
rotateControl: false,
|
||||||
|
fullscreenControl: true,
|
||||||
|
}"
|
||||||
|
style="width: 100%; height: 500px; margin: auto"
|
||||||
|
>
|
||||||
|
<GMapMarker
|
||||||
|
v-for="(marker, index) in markers"
|
||||||
|
:key="index"
|
||||||
|
:position="marker.position"
|
||||||
|
:clickable="true"
|
||||||
|
:draggable="false"
|
||||||
|
@click="openMarker(marker.id)"
|
||||||
|
>
|
||||||
|
<GMapInfoWindow
|
||||||
|
:closeclick="true"
|
||||||
|
:opened="openedMarkerID === marker.id"
|
||||||
|
@closeclick="openMarker(null)"
|
||||||
|
>
|
||||||
|
<div>{{ marker.description }}</div>
|
||||||
|
</GMapInfoWindow>
|
||||||
|
</GMapMarker>
|
||||||
|
</GMapMap>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const openedMarkerID = ref(null);
|
||||||
|
const center = { lat: 37.5488, lng: 127.0440105738147 };
|
||||||
|
const markers = [
|
||||||
|
{
|
||||||
|
description: 'Inspond Co., Ltd.',
|
||||||
|
title: '인스폰드',
|
||||||
|
label: ['라벨'],
|
||||||
|
id: '1',
|
||||||
|
position: {
|
||||||
|
lat: 37.5488,
|
||||||
|
lng: 127.0440105738147,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function openMarker(id) {
|
||||||
|
console.log('huk open, id = ', id);
|
||||||
|
openedMarkerID.value = id;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
85
inspond-nuxt-safekiso/base/pages/support/faq.vue
Normal file
85
inspond-nuxt-safekiso/base/pages/support/faq.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-gray-50">
|
||||||
|
<div class="max-w-7xl mx-auto py-6 space-y-6 sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6">
|
||||||
|
<div class="bg-white">
|
||||||
|
<div
|
||||||
|
class="max-w-7xl mx-auto py-16 px-4 sm:px-6 lg:py-20 lg:px-8"
|
||||||
|
>
|
||||||
|
<div class="lg:grid lg:grid-cols-3 lg:gap-8">
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
class="text-3xl font-extrabold text-gray-900"
|
||||||
|
>
|
||||||
|
자주 묻는 질문과 답변
|
||||||
|
</h2>
|
||||||
|
<p class="mt-4 text-base text-gray-500">
|
||||||
|
궁금하신 내용이 이곳에 없다면
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
@click="
|
||||||
|
$router.push('/support/inquiry')
|
||||||
|
"
|
||||||
|
>일대일 문의</a
|
||||||
|
>
|
||||||
|
메뉴를 통해 질문을 남기실 수 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-12 lg:mt-0 lg:col-span-2">
|
||||||
|
<dl class="space-y-12">
|
||||||
|
<div v-if="listData.length == 0">
|
||||||
|
<dt
|
||||||
|
class="text-lg leading-6 font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
아직 등록된 자주 묻는 질문과 답변이
|
||||||
|
없나요?
|
||||||
|
</dt>
|
||||||
|
<dd
|
||||||
|
class="mt-2 text-base text-gray-500"
|
||||||
|
>
|
||||||
|
네. 아직은 등록된 내용이 없습니다.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="item in listData"
|
||||||
|
v-else
|
||||||
|
:key="item.serial"
|
||||||
|
>
|
||||||
|
<BaseFaqItem1 :item="item" />
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
/*
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
const listData = ref();
|
||||||
|
const inLoadingFlag = ref(false);
|
||||||
|
const statusMessage = ref('데이터를 읽어 오는 중...');
|
||||||
|
|
||||||
|
inLoadingFlag.value = true;
|
||||||
|
const responseJson = await _crossCtl.doComm('list', 'faq:active', {});
|
||||||
|
|
||||||
|
inLoadingFlag.value = false;
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
listData.value = responseJson['data'];
|
||||||
|
} else {
|
||||||
|
statusMessage.value = responseJson['responseMessage'];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
||||||
185
inspond-nuxt-safekiso/base/pages/support/inquiry/index.vue
Normal file
185
inspond-nuxt-safekiso/base/pages/support/inquiry/index.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<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">
|
||||||
|
{{ pageDescription }}
|
||||||
|
</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="makeNewOne"
|
||||||
|
>
|
||||||
|
새 문의 작성
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const inquiryListOptionTags = ['대기중', '검토중', '답변완료', '삭제'];
|
||||||
|
const inquiryListOption = ref(0);
|
||||||
|
|
||||||
|
const pageTitle = '1:1 문의';
|
||||||
|
const pageDescription =
|
||||||
|
'나의 1:1 문의 내역을 확인하고 새로운 문의를 작성할 수 있습니다.';
|
||||||
|
|
||||||
|
const listSource = 'list';
|
||||||
|
const listTarget = 'inquiry:all';
|
||||||
|
|
||||||
|
const deletedListName = 'support-inquiry-deleted';
|
||||||
|
const makeNewTargetName = 'support-inquiry-new';
|
||||||
|
|
||||||
|
const listActions = ['상세보기'];
|
||||||
|
const actionKey = 'serial';
|
||||||
|
const listKeys = ['serial', 'name', 'title', 'status', 'updated'];
|
||||||
|
|
||||||
|
const listHeadings = [
|
||||||
|
{
|
||||||
|
title: '제목',
|
||||||
|
widthRatio: '',
|
||||||
|
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell',
|
||||||
|
key: 'title',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass: 'hidden px-3 py-4 text-sm text-gray-500 lg:table-cell',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '상태',
|
||||||
|
widthRatio: '10',
|
||||||
|
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: '수정일',
|
||||||
|
widthRatio: '15',
|
||||||
|
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
|
||||||
|
key: 'updated',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '작성일',
|
||||||
|
widthRatio: '15',
|
||||||
|
class: 'hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell',
|
||||||
|
key: 'created',
|
||||||
|
hiddenInfo: {
|
||||||
|
headClass: '',
|
||||||
|
dts: [],
|
||||||
|
dds: [],
|
||||||
|
},
|
||||||
|
subClass: 'hidden px-3 py-4 text-sm text-gray-500 sm:table-cell',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const doActionTargetName = 'support-inquiry-view';
|
||||||
|
|
||||||
|
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 if (key == 'status') {
|
||||||
|
return inquiryListOptionTags[val];
|
||||||
|
} else {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
function doAction(tag, target) {
|
||||||
|
console.log('on doAction(), tag=', tag, ', target=', target);
|
||||||
|
|
||||||
|
navigateTo('/support/inquiry/view/' + target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeNewOne() {
|
||||||
|
router.push({
|
||||||
|
name: makeNewTargetName,
|
||||||
|
params: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageMove(targetPageIdex) {
|
||||||
|
console.log('on pageMove(), targetPageIdex=', targetPageIdex);
|
||||||
|
currentPageNumber.value = targetPageIdex;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
const responseJson = await _crossCtl.doComm(listSource, listTarget, {
|
||||||
|
hero: inquiryListOption.value,
|
||||||
|
start: (currentPageNumber.value - 1) * pageSize.value,
|
||||||
|
length: pageSize.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] != 200) {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
} else {
|
||||||
|
currentPageNumber.value = responseJson['currentPageNumber'];
|
||||||
|
totalPageCount.value = responseJson['totalPageCount'];
|
||||||
|
pageSize.value = parseInt(responseJson['pageSize']);
|
||||||
|
recordsTotal.value = responseJson['recordsTotal'];
|
||||||
|
|
||||||
|
listData.value = responseJson['data'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
</script>
|
||||||
244
inspond-nuxt-safekiso/base/pages/support/inquiry/new.vue
Normal file
244
inspond-nuxt-safekiso/base/pages/support/inquiry/new.vue
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="doCreate">
|
||||||
|
<div class="sm:flex sm:items-center">
|
||||||
|
<div class="sm:flex-auto">
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900">
|
||||||
|
{{ newTitle }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm text-gray-700">
|
||||||
|
{{ newDescription }}
|
||||||
|
</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-2"></div>
|
||||||
|
|
||||||
|
<form action="#" class="relative">
|
||||||
|
<div
|
||||||
|
class="border border-gray-300 rounded-lg shadow-sm overflow-hidden focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<label for="title" class="sr-only">Title</label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
v-model="targetTitle"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
class="block w-full border-0 pt-2.5 text-lg font-medium placeholder-gray-500 focus:ring-0"
|
||||||
|
placeholder="제목"
|
||||||
|
/>
|
||||||
|
<label for="description" class="sr-only">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
v-model="targetContent"
|
||||||
|
rows="12"
|
||||||
|
name="description"
|
||||||
|
class="block w-full border-0 py-0 resize-none placeholder-gray-500 focus:ring-0 sm:text-sm"
|
||||||
|
placeholder="내용..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Spacer element to match the height of the toolbar -->
|
||||||
|
<div aria-hidden="true">
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="h-9" />
|
||||||
|
</div>
|
||||||
|
<div class="h-px" />
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="py-px">
|
||||||
|
<div class="h-9" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 inset-x-px">
|
||||||
|
<!-- Actions: These are just examples to demonstrate the concept, replace/wire these up however makes sense for your project. -->
|
||||||
|
<div
|
||||||
|
class="flex flex-nowrap justify-end py-2 px-2 space-x-2 sm:px-3"
|
||||||
|
>
|
||||||
|
<Listbox v-model="labelled" as="div" class="flex-shrink-0">
|
||||||
|
<ListboxLabel class="sr-only">
|
||||||
|
Add a label
|
||||||
|
</ListboxLabel>
|
||||||
|
<div class="relative">
|
||||||
|
<ListboxButton
|
||||||
|
class="relative inline-flex items-center rounded-full py-2 px-2 bg-gray-50 text-sm font-medium text-gray-500 whitespace-nowrap hover:bg-gray-100 sm:px-3"
|
||||||
|
>
|
||||||
|
<TagIcon
|
||||||
|
:class="[
|
||||||
|
labelled.value === null
|
||||||
|
? 'text-gray-300'
|
||||||
|
: 'text-gray-500',
|
||||||
|
'flex-shrink-0 h-5 w-5 sm:-ml-1',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
labelled.value === null
|
||||||
|
? ''
|
||||||
|
: 'text-gray-900',
|
||||||
|
'hidden truncate sm:ml-2 sm:block',
|
||||||
|
]"
|
||||||
|
>{{
|
||||||
|
labelled.value === null
|
||||||
|
? 'Label'
|
||||||
|
: labelled.name
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</ListboxButton>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
leave-active-class="transition ease-in duration-100"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<ListboxOptions
|
||||||
|
class="absolute right-0 z-10 mt-1 w-52 bg-white shadow max-h-56 rounded-lg py-3 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<ListboxOption
|
||||||
|
v-for="label in labels"
|
||||||
|
:key="label.value"
|
||||||
|
v-slot="{ active }"
|
||||||
|
as="template"
|
||||||
|
:value="label"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
active
|
||||||
|
? 'bg-gray-100'
|
||||||
|
: 'bg-white',
|
||||||
|
'cursor-default select-none relative py-2 px-3',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
class="block font-medium truncate"
|
||||||
|
>
|
||||||
|
{{ label.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
</ListboxOptions>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
<base-attachment-ctl1
|
||||||
|
:attachments="attachments"
|
||||||
|
:read-only-flag="false"
|
||||||
|
:update-attachments="updateAttachments"
|
||||||
|
:secure-enabled="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-2 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>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Listbox,
|
||||||
|
ListboxButton,
|
||||||
|
ListboxLabel,
|
||||||
|
ListboxOption,
|
||||||
|
ListboxOptions,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
|
||||||
|
import { PaperClipIcon, TagIcon } from '@heroicons/vue/24/solid';
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = [
|
||||||
|
{ name: '라벨 없음', value: null },
|
||||||
|
{ name: '사이트 이용', value: 'site' },
|
||||||
|
{ name: 'API 문의', value: 'api' },
|
||||||
|
{ name: '기타', value: 'etc' },
|
||||||
|
// More items...
|
||||||
|
];
|
||||||
|
const labelled = ref(labels[0]);
|
||||||
|
|
||||||
|
const newTitle = '1:1 문의 작성';
|
||||||
|
const newDescription =
|
||||||
|
'문의 사항을 적어 주시고 필요한 경우 파일도 첨부하실 수 있습니다.';
|
||||||
|
|
||||||
|
const inPregressFlag = ref(false);
|
||||||
|
|
||||||
|
const targetTitle = ref('');
|
||||||
|
const targetContent = ref('');
|
||||||
|
const attachments = ref([]);
|
||||||
|
const targetStatus = ref(0);
|
||||||
|
|
||||||
|
const actionTarget = 'inquiry';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function updateAttachments(newAttachments) {
|
||||||
|
console.log('newAttachments=', newAttachments);
|
||||||
|
attachments.value = newAttachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCancel() {
|
||||||
|
if (inPregressFlag.value == true) {
|
||||||
|
alert('이전 동작이 아직 진행중입니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCreate() {
|
||||||
|
if (inPregressFlag.value == true) {
|
||||||
|
alert('이전 동작이 아직 진행중입니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetTitle.value == '' || targetContent.value == '') {
|
||||||
|
alert('내용을 입력하셔야 합니다. ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inPregressFlag.value = true;
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('insert', actionTarget, {
|
||||||
|
title: targetTitle.value,
|
||||||
|
question: targetContent.value,
|
||||||
|
attachmentFrom: attachments.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
});
|
||||||
|
inPregressFlag.value = false;
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
alert('오류 : ' + responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
384
inspond-nuxt-safekiso/base/pages/support/inquiry/view/[hero].vue
Normal file
384
inspond-nuxt-safekiso/base/pages/support/inquiry/view/[hero].vue
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="doUpdate">
|
||||||
|
<div class="sm:flex sm:items-center">
|
||||||
|
<div class="sm:flex-auto">
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900">
|
||||||
|
{{ newTitle }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm text-gray-700">
|
||||||
|
{{ newDescription }}
|
||||||
|
</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-2"></div>
|
||||||
|
<div>
|
||||||
|
<form action="#" class="relative">
|
||||||
|
<div
|
||||||
|
class="border border-gray-300 rounded-lg shadow-sm overflow-hidden focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<label for="title" class="sr-only">Title</label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
v-model="targetTitle"
|
||||||
|
:disabled="targetStatus != 0"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
class="block w-full border-0 pt-2.5 text-lg font-medium placeholder-gray-500 focus:ring-0"
|
||||||
|
placeholder="Title"
|
||||||
|
/>
|
||||||
|
<label for="description" class="sr-only">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
v-model="targetContent"
|
||||||
|
rows="8"
|
||||||
|
:disabled="targetStatus != 0"
|
||||||
|
name="description"
|
||||||
|
class="block w-full border-0 py-0 resize-none placeholder-gray-500 focus:ring-0 sm:text-sm"
|
||||||
|
placeholder="Write a description..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Spacer element to match the height of the toolbar -->
|
||||||
|
<div aria-hidden="true">
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="h-9" />
|
||||||
|
</div>
|
||||||
|
<div class="h-px" />
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="py-px">
|
||||||
|
<div class="h-9" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 inset-x-px">
|
||||||
|
<!-- Actions: These are just examples to demonstrate the concept, replace/wire these up however makes sense for your project. -->
|
||||||
|
<div
|
||||||
|
class="flex flex-nowrap justify-end py-2 px-2 space-x-2 sm:px-3"
|
||||||
|
>
|
||||||
|
<Listbox
|
||||||
|
v-model="labelled"
|
||||||
|
as="div"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<ListboxLabel class="sr-only">
|
||||||
|
Add a label
|
||||||
|
</ListboxLabel>
|
||||||
|
<div class="relative">
|
||||||
|
<ListboxButton
|
||||||
|
:disabled="targetStatus != 0"
|
||||||
|
class="relative inline-flex items-center rounded-full py-2 px-2 bg-gray-50 text-sm font-medium text-gray-500 whitespace-nowrap hover:bg-gray-100 sm:px-3"
|
||||||
|
>
|
||||||
|
<TagIcon
|
||||||
|
:class="[
|
||||||
|
labelled.value === null
|
||||||
|
? 'text-gray-300'
|
||||||
|
: 'text-gray-500',
|
||||||
|
'flex-shrink-0 h-5 w-5 sm:-ml-1',
|
||||||
|
]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
labelled.value === null
|
||||||
|
? ''
|
||||||
|
: 'text-gray-900',
|
||||||
|
'hidden truncate sm:ml-2 sm:block',
|
||||||
|
]"
|
||||||
|
>{{
|
||||||
|
labelled.value === null
|
||||||
|
? 'Label'
|
||||||
|
: labelled.name
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</ListboxButton>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
leave-active-class="transition ease-in duration-100"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<ListboxOptions
|
||||||
|
class="absolute right-0 z-10 mt-1 w-52 bg-white shadow max-h-56 rounded-lg py-3 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<ListboxOption
|
||||||
|
v-for="label in labels"
|
||||||
|
:key="label.value"
|
||||||
|
v-slot="{ active }"
|
||||||
|
as="template"
|
||||||
|
:value="label"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
active
|
||||||
|
? 'bg-gray-100'
|
||||||
|
: 'bg-white',
|
||||||
|
'cursor-default select-none relative py-2 px-3',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
class="block font-medium truncate"
|
||||||
|
>
|
||||||
|
{{ label.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
</ListboxOptions>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
<base-attachment-ctl1
|
||||||
|
:attachments="targetAttachmentFrom"
|
||||||
|
:read-only-flag="targetStatus != 0"
|
||||||
|
:update-attachments="updateAttachments"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div v-if="targetStatus == 2">
|
||||||
|
<p class="mt-5 text-sm text-gray-700">답변</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border border-gray-300 rounded-lg shadow-sm overflow-hidden focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<label for="answer" class="sr-only">answer</label>
|
||||||
|
<textarea
|
||||||
|
id="answer"
|
||||||
|
v-model="targetAnswer"
|
||||||
|
disabled
|
||||||
|
rows="8"
|
||||||
|
name="answer"
|
||||||
|
class="block w-full border-0 py-0 resize-none placeholder-gray-500 focus:ring-0 sm:text-sm"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<base-attachment-ctl1
|
||||||
|
:attachments="targetAttachmentTo"
|
||||||
|
:read-only-flag="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 flex justify-between">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
v-if="currentTarget != 'inquiry'"
|
||||||
|
type="button"
|
||||||
|
:class="
|
||||||
|
targetStatus == 0
|
||||||
|
? 'inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500'
|
||||||
|
: '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="doToggle"
|
||||||
|
>
|
||||||
|
{{ targetStatus == 0 ? '삭제' : '복구' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
v-if="targetStatus == 0"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{{ targetStatus == 0 ? '수정' : '확인' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Listbox,
|
||||||
|
ListboxButton,
|
||||||
|
ListboxLabel,
|
||||||
|
ListboxOption,
|
||||||
|
ListboxOptions,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
import { TagIcon } from '@heroicons/vue/24/solid';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
let labels = [
|
||||||
|
{ name: '라벨 없음', value: null },
|
||||||
|
{ name: '공지', value: 'notice' },
|
||||||
|
{ name: '이벤트', value: 'event' },
|
||||||
|
// More items...
|
||||||
|
];
|
||||||
|
const labelled = ref(labels[0]);
|
||||||
|
|
||||||
|
const newTitle = ref('');
|
||||||
|
const newDescription = ref('');
|
||||||
|
|
||||||
|
const contentTitle = ref('');
|
||||||
|
const contentMessageGuide = ref('');
|
||||||
|
|
||||||
|
const currentTarget = ref('inquiry');
|
||||||
|
|
||||||
|
const actionTarget = 'inquiry';
|
||||||
|
|
||||||
|
const inPregressFlag = ref(false);
|
||||||
|
|
||||||
|
const targetTitle = ref('');
|
||||||
|
const targetContent = ref('');
|
||||||
|
const targetAttachmentFrom = ref([]);
|
||||||
|
const targetAnswer = ref('');
|
||||||
|
const targetAttachmentTo = ref([]);
|
||||||
|
const targetStatus = ref(0);
|
||||||
|
|
||||||
|
const inquiryListOptionTags = ['대기중', '검토중', '답변완료', '삭제'];
|
||||||
|
|
||||||
|
let targetCreated = '';
|
||||||
|
|
||||||
|
async function doToggle() {
|
||||||
|
if (targetStatus.value == 0) {
|
||||||
|
const responseJson = await _crossCtl.doComm('delete', actionTarget, {
|
||||||
|
hero: route.params.hero,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
targetStatus.value = 4;
|
||||||
|
alert('ok');
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newTitle.value = '1:1 문의 처리';
|
||||||
|
newDescription.value =
|
||||||
|
'1:1 문의에 답을 입력하면 상태가 즉시 답변 완료로 변하지만, 내용은 추가 수정할 수 있습니다.';
|
||||||
|
|
||||||
|
contentTitle.value = '질문';
|
||||||
|
contentMessageGuide.value = '답변';
|
||||||
|
|
||||||
|
labels = [
|
||||||
|
{ name: '라벨 없음', value: null },
|
||||||
|
{ name: '사이트 이용', value: 'site' },
|
||||||
|
{ name: 'API 문의', value: 'api' },
|
||||||
|
{ name: '기타', value: 'etc' },
|
||||||
|
// More items...
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('route.params.hero=', route.params.hero);
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('select', currentTarget.value, {
|
||||||
|
hero: route.params.hero,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
console.log(responseJson['data']);
|
||||||
|
|
||||||
|
targetTitle.value = responseJson['data'][0]['title'];
|
||||||
|
targetContent.value = responseJson['data'][0]['question'];
|
||||||
|
targetAttachmentFrom.value = JSON.parse(
|
||||||
|
responseJson['data'][0]['attachment_from']
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('huk targetAttachmentFrom.value = ', targetAttachmentFrom);
|
||||||
|
targetAnswer.value = responseJson['data'][0]['answer'];
|
||||||
|
targetAttachmentTo.value = JSON.parse(
|
||||||
|
responseJson['data'][0]['attachment_to']
|
||||||
|
);
|
||||||
|
|
||||||
|
const tmpFlags =
|
||||||
|
responseJson['data'][0]['flags'] != null
|
||||||
|
? responseJson['data'][0]['flags']
|
||||||
|
: '[]';
|
||||||
|
|
||||||
|
const flags = JSON.parse(tmpFlags);
|
||||||
|
for (let i = 0; i < flags.length; i++) {
|
||||||
|
const flag = flags[i];
|
||||||
|
for (let j = 0; j < labels.length; j++) {
|
||||||
|
if (flag == labels[j]['value']) {
|
||||||
|
labelled.value = labels[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetCreated = responseJson['data'][0]['created'];
|
||||||
|
|
||||||
|
targetStatus.value = responseJson['data'][0]['status'];
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('huk route.params.target=', route.params.target);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
async function doCancel() {
|
||||||
|
if (inPregressFlag.value == true) {
|
||||||
|
alert('이전 동작이 아직 진행중입니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAttachments(newAttachments) {
|
||||||
|
console.log('newAttachments=', newAttachments);
|
||||||
|
targetAttachmentFrom.value = newAttachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doUpdate() {
|
||||||
|
if (inPregressFlag.value == true) {
|
||||||
|
alert('이전 동작이 아직 진행중입니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetTitle.value == '' || targetContent.value == '') {
|
||||||
|
alert('내용을 입력하셔야 합니다. ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetStatus.value != 0) {
|
||||||
|
router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inPregressFlag.value = true;
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('update', actionTarget, {
|
||||||
|
hero: route.params.hero,
|
||||||
|
title: targetTitle.value,
|
||||||
|
question: targetContent.value,
|
||||||
|
attachmentFrom: targetAttachmentFrom.value,
|
||||||
|
flags: labelled.value['value']
|
||||||
|
? JSON.stringify([labelled.value['value']])
|
||||||
|
: JSON.stringify([]),
|
||||||
|
status: targetStatus.value,
|
||||||
|
});
|
||||||
|
inPregressFlag.value = false;
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
alert('오류 : ' + responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
132
inspond-nuxt-safekiso/base/pages/support/notice.vue
Normal file
132
inspond-nuxt-safekiso/base/pages/support/notice.vue
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<ul v-if="inLoadingFlag">
|
||||||
|
<li>
|
||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<div class="rounded-md bg-green-50 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<!-- Heroicon name: check-circle -->
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-green-400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm font-medium text-green-800">
|
||||||
|
{{ statusMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto pl-3">
|
||||||
|
<div class="-mx-1.5 -my-1.5"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul v-else-if="statusMessage != null">
|
||||||
|
<li>
|
||||||
|
<div class="rounded-md bg-red-50 p-4 m-5">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<!-- Heroicon name: x-circle -->
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-red-400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800">
|
||||||
|
오류가 발생하였습니다.
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 text-sm text-red-700">
|
||||||
|
<ul class="list-disc pl-5 space-y-1">
|
||||||
|
<li>
|
||||||
|
{{ statusMessage }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<ul v-if="listData.length > 0">
|
||||||
|
<li v-for="noticeItem in listData" :key="noticeItem.serial">
|
||||||
|
<BaseNoticeItem1 :item="noticeItem" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="rounded-md bg-red-50 p-4 m-5">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<!-- Heroicon name: x-circle -->
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-red-400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800">
|
||||||
|
등록된 공지가 없습니다.
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const listData = ref();
|
||||||
|
const inLoadingFlag = ref(true);
|
||||||
|
const statusMessage = ref('데이터를 읽어 오는 중...');
|
||||||
|
|
||||||
|
inLoadingFlag.value = true;
|
||||||
|
const responseJson = await _crossCtl.doComm('local/list', 'notice', {});
|
||||||
|
|
||||||
|
inLoadingFlag.value = false;
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
listData.value = responseJson['data'];
|
||||||
|
statusMessage.value = null;
|
||||||
|
} else {
|
||||||
|
statusMessage.value = responseJson['responseMessage'];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
||||||
451
inspond-nuxt-safekiso/base/pages/user/info.vue
Normal file
451
inspond-nuxt-safekiso/base/pages/user/info.vue
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<div class="divide-y divide-gray-200">
|
||||||
|
<form class="lg:col-span-9" @submit.prevent="doUpdateInfo">
|
||||||
|
<!-- Profile section -->
|
||||||
|
<div class="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
사용자 정보 확인, 변경
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
일부 정보는 다른 사용자들에게 보여질 수 있으니 신중하게
|
||||||
|
입력해 주세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div class="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
for="email"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>이메일</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="email"
|
||||||
|
disabled
|
||||||
|
type="text"
|
||||||
|
name="email"
|
||||||
|
autocomplete="email"
|
||||||
|
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="username"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
사용자 이름
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 rounded-md shadow-sm flex">
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="displayName"
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
autocomplete="username"
|
||||||
|
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>
|
||||||
|
<label
|
||||||
|
for="phone"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
전화번호
|
||||||
|
</label>
|
||||||
|
<div class="mt-1 rounded-md shadow-sm flex">
|
||||||
|
<input
|
||||||
|
id="phone"
|
||||||
|
v-model="phone"
|
||||||
|
type="text"
|
||||||
|
name="phone"
|
||||||
|
autocomplete="phone"
|
||||||
|
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>
|
||||||
|
<label
|
||||||
|
for="about"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
간단한 소개
|
||||||
|
</label>
|
||||||
|
<div class="mt-1">
|
||||||
|
<textarea
|
||||||
|
id="about"
|
||||||
|
v-model="memo"
|
||||||
|
name="about"
|
||||||
|
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>
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
프로필 사진
|
||||||
|
</label>
|
||||||
|
<div class="mt-2 flex items-center space-x-5">
|
||||||
|
<span
|
||||||
|
v-if="photoUrl == ''"
|
||||||
|
class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-full w-full text-gray-300"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="inline-block h-12 w-12 rounded-full border"
|
||||||
|
:src="photoUrl"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2 sm:col-span-2 pt-3">
|
||||||
|
<div class="mt-1 flex rounded-md shadow-sm">
|
||||||
|
<div
|
||||||
|
class="relative flex items-stretch flex-grow focus-within:z-10"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="photoUrl"
|
||||||
|
v-model="photoUrl"
|
||||||
|
type="text"
|
||||||
|
name="photoUrl"
|
||||||
|
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-none rounded-l-md sm:text-sm border-gray-300"
|
||||||
|
placeholder="http://"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500">
|
||||||
|
프로필로 사용하실 이미지의 주소를
|
||||||
|
입력하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mt-2 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md"
|
||||||
|
@dragover.prevent
|
||||||
|
@dragenter.prevent
|
||||||
|
@drop.prevent="
|
||||||
|
filesChange(
|
||||||
|
'upload-file',
|
||||||
|
$event.dataTransfer.files
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="space-y-1 text-center">
|
||||||
|
<svg
|
||||||
|
class="mx-auto h-12 w-12 text-gray-400"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<div class="flex text-sm text-gray-600">
|
||||||
|
<label
|
||||||
|
for="file-upload"
|
||||||
|
class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
@click="
|
||||||
|
$refs.input_file.click()
|
||||||
|
"
|
||||||
|
>여기</a
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="input_file"
|
||||||
|
type="file"
|
||||||
|
name="upload-file"
|
||||||
|
accept=".jpg,.jpeg,.png"
|
||||||
|
hidden
|
||||||
|
@change="
|
||||||
|
filesChange(
|
||||||
|
$event.target.name,
|
||||||
|
$event.target.files
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p class="pl-1">
|
||||||
|
를 눌러 업로드 하시거나 마우스로
|
||||||
|
이곳에 끌어 놓아 주세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
PNG, JPG 최대 1MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy section -->
|
||||||
|
<div class="pt-0">
|
||||||
|
<div class="mt-4 py-4 px-4 flex justify-end sm:px-6">
|
||||||
|
<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="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="lg:col-span-9" @submit.prevent="doUpdatePassword">
|
||||||
|
<div class="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
비밀번호 변경
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
이전 비밀번호와 새로운 비밀번호를 두번 정확하게 입력해
|
||||||
|
주셔야 합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div class="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
for="passwordCurrent"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>현재 비밀번호</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="passwordCurrent"
|
||||||
|
v-model="passwordCurrent"
|
||||||
|
type="password"
|
||||||
|
name="passwordCurrent"
|
||||||
|
autocomplete="passwordCurrent"
|
||||||
|
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 grid grid-cols-12 gap-6">
|
||||||
|
<div class="col-span-12 sm:col-span-6">
|
||||||
|
<label
|
||||||
|
for="password"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>새로운 비밀번호</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
autocomplete="password"
|
||||||
|
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">
|
||||||
|
<label
|
||||||
|
for="password2"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>비밀번호 확인</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password2"
|
||||||
|
v-model="password2"
|
||||||
|
type="password"
|
||||||
|
name="password2"
|
||||||
|
autocomplete="password2"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy section -->
|
||||||
|
<div class="pt-0">
|
||||||
|
<div class="mt-4 py-4 px-4 flex justify-end sm:px-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
비밀번호 저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="lg:col-span-9" @submit.prevent="doWithdrawal">
|
||||||
|
<div class="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
회원 탈퇴
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
탈퇴 신청을 하시면 계정은 탈퇴처리 되며 일정기간 동일한
|
||||||
|
이메일로 재가입을 하실 수 없습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Privacy section -->
|
||||||
|
<div class="pt-0">
|
||||||
|
<div class="mt-4 py-4 px-4 flex justify-end sm:px-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
회원 탈퇴
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const email = ref(_crossCtl.userProfile['email']);
|
||||||
|
const displayName = ref(_crossCtl.userProfile['displayName']);
|
||||||
|
const photoUrl = ref(_crossCtl.userProfile['photoUrl']);
|
||||||
|
const phone = ref(_crossCtl.userProfile['phone']);
|
||||||
|
const memo = ref(_crossCtl.userProfile['memo']);
|
||||||
|
|
||||||
|
const passwordCurrent = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const password2 = ref('');
|
||||||
|
|
||||||
|
// email: '1@1', displayName: '1@1', phone: '', memo: ''
|
||||||
|
|
||||||
|
async function doUpdateInfo() {
|
||||||
|
const responseJson = await _crossCtl.doComm('update', 'profile', {
|
||||||
|
displayName: displayName.value,
|
||||||
|
photoUrl: photoUrl.value,
|
||||||
|
infos: {
|
||||||
|
email: email.value,
|
||||||
|
phone: phone.value,
|
||||||
|
memo: memo.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doUpdatePassword() {
|
||||||
|
const responseJson = await _crossCtl.doComm('update', 'password', {
|
||||||
|
password_current: passwordCurrent.value,
|
||||||
|
password_new: password.value,
|
||||||
|
password_again: password2.value,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
alert('ok');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCancel() {
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doWithdrawal() {
|
||||||
|
_crossCtl.openModal(
|
||||||
|
'confirm',
|
||||||
|
'탈퇴 확인',
|
||||||
|
'정말로 탈퇴하시겠습니까?',
|
||||||
|
['탈퇴', '취소'],
|
||||||
|
(serial, btnIdx) => {
|
||||||
|
console.log('btnIdx=', btnIdx);
|
||||||
|
if (btnIdx == 0) {
|
||||||
|
router.push('/user/withdrawal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
/*
|
||||||
|
if (window.confirm('탈퇴하시겠습니까?')) {
|
||||||
|
router.push('/user/withdrawal');
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
async function filesChange(fieldName, fileList) {
|
||||||
|
// handle file changes
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
if (!fileList.length) return;
|
||||||
|
|
||||||
|
// append the files to FormData
|
||||||
|
Array.from(Array(fileList.length).keys()).map((x) => {
|
||||||
|
formData.append(fieldName, fileList[x], fileList[x].name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// save it
|
||||||
|
console.log('formData=', formData);
|
||||||
|
|
||||||
|
formData.append('target', 'just');
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doUpload('just', formData);
|
||||||
|
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
photoUrl.value =
|
||||||
|
_crossCtl.config['API_BASE_URL'].replace('/api/', '') +
|
||||||
|
responseJson['files'][0]['localUrl'];
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
149
inspond-nuxt-safekiso/base/pages/user/password-reset.vue
Normal file
149
inspond-nuxt-safekiso/base/pages/user/password-reset.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
|
<!--
|
||||||
|
This example requires updating your template:
|
||||||
|
|
||||||
|
```
|
||||||
|
<html class="h-full bg-gray-50">
|
||||||
|
<body class="h-full">
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
class="mt-6 text-center text-3xl font-extrabold text-gray-900"
|
||||||
|
>
|
||||||
|
비밀번호 찾기
|
||||||
|
</h2>
|
||||||
|
<!--
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600">
|
||||||
|
계정이 없으신 경우
|
||||||
|
{{ ' ' }}
|
||||||
|
<a
|
||||||
|
href="javascript:void()"
|
||||||
|
class="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
@click="navigateTo('/user/signup')"
|
||||||
|
>
|
||||||
|
이곳에서 회원가입
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
-->
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600">
|
||||||
|
계정이 있는 경우
|
||||||
|
{{ ' ' }}
|
||||||
|
<a
|
||||||
|
href="javascript:void()"
|
||||||
|
class="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
@click="navigateTo('/user/signin')"
|
||||||
|
>
|
||||||
|
이곳에서 로그인
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form class="mt-8 space-y-6" @submit.prevent="passwordReset">
|
||||||
|
<input type="hidden" name="remember" value="true" />
|
||||||
|
<div class="rounded-md shadow-sm -space-y-px">
|
||||||
|
<div>
|
||||||
|
<label for="email-address" class="sr-only"
|
||||||
|
>이메일 주소</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="email-address"
|
||||||
|
v-model="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
autocomplete="email"
|
||||||
|
required=""
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="이메일 주소"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600">
|
||||||
|
입력하신 이메일 주소로 비밀번호 리셋 링크를 보내
|
||||||
|
드립니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent 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"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute left-0 inset-y-0 flex items-center pl-3"
|
||||||
|
>
|
||||||
|
<LockClosedIcon
|
||||||
|
class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
링크 요청
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { LockClosedIcon } from '@heroicons/vue/24/solid/index.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
class: 'h-full bg-white',
|
||||||
|
},
|
||||||
|
bodyAttrs: {
|
||||||
|
class: 'h-full',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const email = ref('');
|
||||||
|
|
||||||
|
async function passwordReset() {
|
||||||
|
const responseJson = await _crossCtl.doComm('reset', '', {
|
||||||
|
userName: email.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
_utils.log('debug', 'responseJson=', responseJson);
|
||||||
|
|
||||||
|
switch (responseJson['responseMessage']) {
|
||||||
|
case 'no user found':
|
||||||
|
alert('no user found');
|
||||||
|
break;
|
||||||
|
case 'ok':
|
||||||
|
alert(
|
||||||
|
'비밀번호 복구 메일을 발송하였습니다. 잠시 후에 이메일 수신함을 확인해 주세요.'
|
||||||
|
);
|
||||||
|
// window.location.replace('/');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
163
inspond-nuxt-safekiso/base/pages/user/profile/[pid]/index.vue
Normal file
163
inspond-nuxt-safekiso/base/pages/user/profile/[pid]/index.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<section class="bg-white overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="relative max-w-7xl mx-auto pt-20 pb-12 px-4 sm:px-6 lg:px-8 lg:py-20"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="absolute top-full left-0 transform translate-x-80 -translate-y-24 lg:hidden"
|
||||||
|
width="784"
|
||||||
|
height="404"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 784 404"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
id="e56e3f81-d9c1-4b83-a3ba-0d0ac8c32f32"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="4"
|
||||||
|
height="4"
|
||||||
|
class="text-gray-200"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
width="784"
|
||||||
|
height="404"
|
||||||
|
fill="url(#e56e3f81-d9c1-4b83-a3ba-0d0ac8c32f32)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class="hidden lg:block absolute right-full top-1/2 transform translate-x-1/2 -translate-y-1/2"
|
||||||
|
width="404"
|
||||||
|
height="784"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 404 784"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
id="56409614-3d62-4985-9a10-7ca758a8f4f0"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="4"
|
||||||
|
height="4"
|
||||||
|
class="text-gray-200"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
width="404"
|
||||||
|
height="784"
|
||||||
|
fill="url(#56409614-3d62-4985-9a10-7ca758a8f4f0)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="relative lg:flex lg:items-center">
|
||||||
|
<div class="w-64 h-64">
|
||||||
|
<BaseAvater1 :image-size="64" :image-url="photoUrl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative lg:ml-10">
|
||||||
|
<svg
|
||||||
|
class="absolute top-0 left-0 transform -translate-x-8 -translate-y-24 h-36 w-36 text-indigo-200 opacity-50"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 144 144"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-width="2"
|
||||||
|
d="M41.485 15C17.753 31.753 1 59.208 1 89.455c0 24.664 14.891 39.09 32.109 39.09 16.287 0 28.386-13.03 28.386-28.387 0-15.356-10.703-26.524-24.663-26.524-2.792 0-6.515.465-7.446.93 2.327-15.821 17.218-34.435 32.11-43.742L41.485 15zm80.04 0c-23.268 16.753-40.02 44.208-40.02 74.455 0 24.664 14.891 39.09 32.109 39.09 15.822 0 28.386-13.03 28.386-28.387 0-15.356-11.168-26.524-25.129-26.524-2.792 0-6.049.465-6.98.93 2.327-15.821 16.753-34.435 31.644-43.742L121.525 15z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<blockquote class="relative">
|
||||||
|
<div
|
||||||
|
class="text-2xl leading-9 font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{ memo }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<footer class="mt-8">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0 lg:hidden">
|
||||||
|
<img
|
||||||
|
class="h-12 w-12 rounded-full"
|
||||||
|
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4 lg:ml-0">
|
||||||
|
<div
|
||||||
|
class="text-base font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{ displayName }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-base font-medium text-indigo-600"
|
||||||
|
>
|
||||||
|
{{ email }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const displayName = ref(_crossCtl.userProfile['displayName']);
|
||||||
|
const email = ref('');
|
||||||
|
const photoUrl = ref(_crossCtl.userProfile['photoUrl']);
|
||||||
|
const memo = ref(_crossCtl.userProfile['memo']);
|
||||||
|
|
||||||
|
let userInfo = {};
|
||||||
|
|
||||||
|
const hero = route.params.pid;
|
||||||
|
|
||||||
|
console.log('hero=', hero);
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('select', 'profile', {
|
||||||
|
hero: hero,
|
||||||
|
});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
userInfo = responseJson['data'][0];
|
||||||
|
const tmpSubInfos = userInfo.infos;
|
||||||
|
email.value = tmpSubInfos['email'];
|
||||||
|
displayName.value = userInfo['display_name'];
|
||||||
|
photoUrl.value = userInfo['photo_url'];
|
||||||
|
memo.value = tmpSubInfos['memo'];
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
153
inspond-nuxt-safekiso/base/pages/user/reset-password.vue
Normal file
153
inspond-nuxt-safekiso/base/pages/user/reset-password.vue
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
|
<!--
|
||||||
|
This example requires updating your template:
|
||||||
|
|
||||||
|
```
|
||||||
|
<html class="h-full bg-gray-50">
|
||||||
|
<body class="h-full">
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
class="mt-6 text-center text-3xl font-extrabold text-gray-900"
|
||||||
|
>
|
||||||
|
비밀번호 변경
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600">
|
||||||
|
계정이 있는 경우
|
||||||
|
{{ ' ' }}
|
||||||
|
<a
|
||||||
|
href="javascript:void()"
|
||||||
|
class="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
@click="navigateTo('/user/signin')"
|
||||||
|
>
|
||||||
|
이곳에서 로그인
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form class="mt-8 space-y-6" @submit.prevent="doReset">
|
||||||
|
<input type="hidden" name="remember" value="true" />
|
||||||
|
<div class="rounded-md shadow-sm -space-y-px">
|
||||||
|
<div>
|
||||||
|
<label for="password" class="sr-only"
|
||||||
|
>새 로그인 비밀번호</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required=""
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="비밀번호"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password2" class="sr-only"
|
||||||
|
>비밀번호 확인</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password2"
|
||||||
|
v-model="password2"
|
||||||
|
name="password2"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password2"
|
||||||
|
required=""
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="비밀번호 확인"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent 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"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute left-0 inset-y-0 flex items-center pl-3"
|
||||||
|
>
|
||||||
|
<LockClosedIcon
|
||||||
|
class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
변경 신청
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { LockClosedIcon } from '@heroicons/vue/24/solid/index.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
class: 'h-full bg-white',
|
||||||
|
},
|
||||||
|
bodyAttrs: {
|
||||||
|
class: 'h-full',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
console.log('huk query = ', route.query);
|
||||||
|
|
||||||
|
const hero = ref(route.query.key);
|
||||||
|
const password = ref('');
|
||||||
|
const password2 = ref('');
|
||||||
|
|
||||||
|
async function doReset() {
|
||||||
|
if (password.value != password2.value) {
|
||||||
|
alert('비밀번호가 일치하지 않습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('update', 'password:reset', {
|
||||||
|
hero: hero.value,
|
||||||
|
passwordNew: password.value,
|
||||||
|
passwordAgain: password.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
_utils.log('debug', 'responseJson=', responseJson);
|
||||||
|
|
||||||
|
switch (responseJson['responseMessage']) {
|
||||||
|
case 'Expired reset request':
|
||||||
|
alert('이미 사용된 비밀번호 리셋 링크입니다.');
|
||||||
|
break;
|
||||||
|
case 'ok':
|
||||||
|
alert('새로운 비밀번호가 설정되었습니다.');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
219
inspond-nuxt-safekiso/base/pages/user/signin.vue
Normal file
219
inspond-nuxt-safekiso/base/pages/user/signin.vue
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="bg-white min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="/kiso_ci_1.png"
|
||||||
|
alt="KISO CI"
|
||||||
|
width="500"
|
||||||
|
height="600"
|
||||||
|
/>
|
||||||
|
<!--
|
||||||
|
<h2
|
||||||
|
class="mt-6 text-center text-3xl font-extrabold text-gray-900"
|
||||||
|
>
|
||||||
|
로그인
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600">
|
||||||
|
계정이 없으신 경우
|
||||||
|
{{ ' ' }}
|
||||||
|
<a
|
||||||
|
href="/user/signup"
|
||||||
|
class="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
>
|
||||||
|
이곳에서 회원가입
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
<form class="mt-8 space-y-6" @submit.prevent="signin">
|
||||||
|
<input type="hidden" name="remember" value="true" />
|
||||||
|
<div class="rounded-md shadow-sm -space-y-px">
|
||||||
|
<div>
|
||||||
|
<label for="email-address" class="sr-only"
|
||||||
|
>이메일 주소</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="email-address"
|
||||||
|
v-model="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
autocomplete="email"
|
||||||
|
required="true"
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="이메일 주소"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="sr-only"
|
||||||
|
>로그인 비밀번호</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required="true"
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="비밀번호"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id="remember-me"
|
||||||
|
v-model="rememberMeFlag"
|
||||||
|
name="remember-me"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="remember-me"
|
||||||
|
class="ml-2 block text-sm text-gray-900"
|
||||||
|
>
|
||||||
|
아이디 저장
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm">
|
||||||
|
<a
|
||||||
|
href="javascript:void()"
|
||||||
|
class="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
@click="navigateTo('/user/password-reset')"
|
||||||
|
>
|
||||||
|
비밀번호 찾기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent 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"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute left-0 inset-y-0 flex items-center pl-3"
|
||||||
|
>
|
||||||
|
<LockClosedIcon
|
||||||
|
class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
로그인
|
||||||
|
</button>
|
||||||
|
<!--
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mt-3 group relative w-full flex justify-center py-2 px-4 border border-transparent 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('/user/signup')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute left-0 inset-y-0 flex items-center pl-3"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
신규 가입
|
||||||
|
</button>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This example requires updating your template:
|
||||||
|
|
||||||
|
```
|
||||||
|
<html class="h-full bg-gray-50">
|
||||||
|
<body class="h-full">
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { LockClosedIcon } from '@heroicons/vue/24/solid/index.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
class: 'h-full bg-white',
|
||||||
|
},
|
||||||
|
bodyAttrs: {
|
||||||
|
class: 'h-full',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const email = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const rememberMeFlag = ref(false);
|
||||||
|
|
||||||
|
const cookieNameEmail = '/user/signin:email';
|
||||||
|
|
||||||
|
const cachedEmail = _utils.getCookie(cookieNameEmail);
|
||||||
|
console.log('cachedEmail=', cachedEmail);
|
||||||
|
|
||||||
|
if (cachedEmail != null) {
|
||||||
|
email.value = cachedEmail;
|
||||||
|
rememberMeFlag.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signin() {
|
||||||
|
// alert(email.value);
|
||||||
|
const normalizedInfo = {
|
||||||
|
provider: 'id/password',
|
||||||
|
id: email.value,
|
||||||
|
name: email.value,
|
||||||
|
email: email.value,
|
||||||
|
photo: '',
|
||||||
|
roleTag: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const infoString = JSON.stringify(normalizedInfo);
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('signin', '', {
|
||||||
|
type: 0,
|
||||||
|
id: email.value,
|
||||||
|
token: password.value,
|
||||||
|
info: infoString,
|
||||||
|
});
|
||||||
|
|
||||||
|
_utils.log('debug', 'responseJson=', responseJson);
|
||||||
|
|
||||||
|
switch (responseJson['responseMessage']) {
|
||||||
|
case 'wrong password count limit exceeded : 5':
|
||||||
|
alert(
|
||||||
|
'비밀번호가 다섯번 틀려 로그인 할 수 없습니다. 비밀번호 찾기를 시도해 보세요.'
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'no user found':
|
||||||
|
alert('아이디를 확인해 주세요.');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bad password':
|
||||||
|
alert('비밀번호가 일치하지 않습니다.');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ok':
|
||||||
|
// alert('login ok');
|
||||||
|
if (rememberMeFlag.value == true) {
|
||||||
|
_utils.setCookie('/user/signin:email', email.value, 10000);
|
||||||
|
} else {
|
||||||
|
_utils.rmvCookie('/user/signin:email');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.replace('/');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
192
inspond-nuxt-safekiso/base/pages/user/signup.vue
Normal file
192
inspond-nuxt-safekiso/base/pages/user/signup.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
|
<!--
|
||||||
|
This example requires updating your template:
|
||||||
|
|
||||||
|
```
|
||||||
|
<html class="h-full bg-gray-50">
|
||||||
|
<body class="h-full">
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="bg-white min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
class="mt-6 text-center text-3xl font-extrabold text-gray-900"
|
||||||
|
>
|
||||||
|
계정등록
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600">
|
||||||
|
계정이 있는 경우
|
||||||
|
{{ ' ' }}
|
||||||
|
<a
|
||||||
|
href="javascript:void()"
|
||||||
|
class="font-medium text-indigo-600 hover:text-indigo-500"
|
||||||
|
@click="navigateTo('/user/signin')"
|
||||||
|
>
|
||||||
|
이곳에서 로그인
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form class="mt-8 space-y-6" @submit.prevent="signup">
|
||||||
|
<input type="hidden" name="remember" value="true" />
|
||||||
|
<div class="rounded-md shadow-sm -space-y-px">
|
||||||
|
<div>
|
||||||
|
<label for="email-address" class="sr-only"
|
||||||
|
>이메일 주소</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="email-address"
|
||||||
|
v-model="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
autocomplete="email"
|
||||||
|
required=""
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="이메일 주소"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="sr-only"
|
||||||
|
>로그인 비밀번호</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required=""
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="비밀번호"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password2" class="sr-only"
|
||||||
|
>비밀번호 확인</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password2"
|
||||||
|
v-model="password2"
|
||||||
|
name="password2"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password2"
|
||||||
|
required=""
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="비밀번호 확인"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent 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"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute left-0 inset-y-0 flex items-center pl-3"
|
||||||
|
>
|
||||||
|
<LockClosedIcon
|
||||||
|
class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
계정등록
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { LockClosedIcon } from '@heroicons/vue/24/solid/index.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
class: 'h-full bg-white',
|
||||||
|
},
|
||||||
|
bodyAttrs: {
|
||||||
|
class: 'h-full',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const email = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const password2 = ref('');
|
||||||
|
|
||||||
|
async function signup() {
|
||||||
|
if (password.value != password2.value) {
|
||||||
|
alert('비밀번호가 일치하지 않습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = await _crossCtl.doComm('signup', '', {
|
||||||
|
userName: email.value,
|
||||||
|
password: password.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
_utils.log('debug', 'responseJson=', responseJson);
|
||||||
|
|
||||||
|
switch (responseJson['responseMessage']) {
|
||||||
|
case 'not in a white list':
|
||||||
|
alert(
|
||||||
|
'사전 승인이 필요합니다. 확인을 누르시면 안내 페이지로 이동합니다.'
|
||||||
|
);
|
||||||
|
navigateTo('/doc/contract');
|
||||||
|
break;
|
||||||
|
case 'ok':
|
||||||
|
alert('가입 완료. 확인을 누르시면 로그인 화면으로 이동합니다.');
|
||||||
|
navigateTo('/user/signin');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
'huk _crossCtl.isSignUpInfoNoticed = ',
|
||||||
|
_crossCtl.isSignUpInfoNoticed
|
||||||
|
);
|
||||||
|
if (_crossCtl.isSignUpInfoNoticed == false) {
|
||||||
|
_crossCtl.isSignUpInfoNoticed = true;
|
||||||
|
console.log('open modal');
|
||||||
|
|
||||||
|
_crossCtl.openModal(
|
||||||
|
'info',
|
||||||
|
'계정등록 안내',
|
||||||
|
'본 서비스의 회원 가입은 정식 계약 신청 후 서버에 등록된 이메일에 대해서만 가능합니다.' +
|
||||||
|
"만약 정식 서비스 계약을 체결하지 않으셨다면 먼저 '서비스 이용 절차' 부분의 내용을 참고해 주시기 바랍니다.",
|
||||||
|
['확인', '서비스 이용 절차 확인'],
|
||||||
|
(serial, btnIdx) => {
|
||||||
|
console.log('btnIdx=', btnIdx);
|
||||||
|
|
||||||
|
if (btnIdx == 1) {
|
||||||
|
// navigateTo('/#service_use_agreement');
|
||||||
|
window.location.href = '/#service-use-agreement';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
64
inspond-nuxt-safekiso/base/pages/user/withdrawal.vue
Normal file
64
inspond-nuxt-safekiso/base/pages/user/withdrawal.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<div class="bg-white">
|
||||||
|
<div
|
||||||
|
class="max-w-7xl mx-auto text-center py-12 px-4 sm:px-6 lg:py-16 lg:px-8"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl"
|
||||||
|
>
|
||||||
|
<span class="block">회원 탈퇴</span>
|
||||||
|
<span class="block">탈퇴하시겠습니까?</span>
|
||||||
|
</h2>
|
||||||
|
<p class="mt-4 text-lg leading-6 text-indigo-200">
|
||||||
|
탈퇴 후에는 일정 기간 동일 이메일로 가입이 불가합니다.
|
||||||
|
</p>
|
||||||
|
<div class="mt-8 flex justify-center">
|
||||||
|
<div class="inline-flex rounded-md shadow">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
|
||||||
|
@click="doWithdrawal"
|
||||||
|
>
|
||||||
|
탈퇴
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 inline-flex">
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200"
|
||||||
|
@click="$router.push('/')"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'check-auth-user',
|
||||||
|
layout: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
class: 'h-full bg-white',
|
||||||
|
},
|
||||||
|
bodyAttrs: {
|
||||||
|
class: 'h-full',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function doWithdrawal() {
|
||||||
|
const responseJson = await _crossCtl.doComm('withdrawal', '', {});
|
||||||
|
console.log('responseJson=', responseJson);
|
||||||
|
if (responseJson['responseMessage'] == 'ok') {
|
||||||
|
window.location.replace('/');
|
||||||
|
} else {
|
||||||
|
alert(responseJson['responseMessage']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
38
inspond-nuxt-safekiso/base/plugins/!.ts
Normal file
38
inspond-nuxt-safekiso/base/plugins/!.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
// _utils.log('defineNuxtPlugin() of !.ts executed...');
|
||||||
|
_crossCtl.setConfig(useRuntimeConfig().public);
|
||||||
|
|
||||||
|
/*
|
||||||
|
nuxtApp.hook('app:created', () => {
|
||||||
|
_utils.log('nuxtApp.hook, app:created');
|
||||||
|
});
|
||||||
|
nuxtApp.hook('app:beforeMount', () => {
|
||||||
|
_utils.log('nuxtApp.hook, app:beforeMount');
|
||||||
|
});
|
||||||
|
nuxtApp.hook('app:mounted', () => {
|
||||||
|
_utils.log('nuxtApp.hook, app:mounted');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
nuxtApp.hook('vue:error', (..._args) => {
|
||||||
|
console.log('vue:error');
|
||||||
|
// if (process.client) {
|
||||||
|
// console.log(..._args)
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
nuxtApp.hook('app:error', (..._args) => {
|
||||||
|
console.log('app:error');
|
||||||
|
// if (process.client) {
|
||||||
|
// console.log(..._args)
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
nuxtApp.vueApp.config.errorHandler = (..._args) => {
|
||||||
|
console.log('global error handler');
|
||||||
|
// if (process.client) {
|
||||||
|
// console.log(..._args);
|
||||||
|
// }
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
});
|
||||||
44
inspond-nuxt-safekiso/base/plugins/customDateTimeFormat.ts
Normal file
44
inspond-nuxt-safekiso/base/plugins/customDateTimeFormat.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export default defineNuxtPlugin((/* nuxtApp */) => {
|
||||||
|
// _utils.log('defineNuxtPlugin() of customDateTimeFormat.ts executed...');
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
customFormat: (rawtime) => {
|
||||||
|
const date: any = new Date(rawtime);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
|
let hh = date.getHours();
|
||||||
|
const mm = date.getMinutes();
|
||||||
|
|
||||||
|
const AmOrPm = hh <= 12 ? '오전' : '오후';
|
||||||
|
hh = hh % 12 || 12;
|
||||||
|
|
||||||
|
const diffInSec = Math.floor((Date.now() - date) / 1000);
|
||||||
|
if (diffInSec < 30) {
|
||||||
|
return '조금 전';
|
||||||
|
}
|
||||||
|
if (diffInSec < 59) {
|
||||||
|
return diffInSec + '초 전';
|
||||||
|
}
|
||||||
|
const diffInMin = Math.floor(diffInSec / 60);
|
||||||
|
if (diffInMin < 59) {
|
||||||
|
return diffInMin + '분 전';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
year +
|
||||||
|
'년 ' +
|
||||||
|
month +
|
||||||
|
'월' +
|
||||||
|
' ' +
|
||||||
|
day +
|
||||||
|
'일, ' +
|
||||||
|
AmOrPm +
|
||||||
|
' ' +
|
||||||
|
hh +
|
||||||
|
':' +
|
||||||
|
mm
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
17
inspond-nuxt-safekiso/base/plugins/dayjs.ts
Normal file
17
inspond-nuxt-safekiso/base/plugins/dayjs.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime.js';
|
||||||
|
import 'dayjs/locale/ko';
|
||||||
|
|
||||||
|
dayjs.locale('ko');
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((/* nuxtApp */) => {
|
||||||
|
// _utils.log('defineNuxtPlugin() of dayjs.ts executed...');
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
dayjs: (rawDateTime) => {
|
||||||
|
return dayjs(rawDateTime);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
9
inspond-nuxt-safekiso/base/plugins/quill.client.js
Normal file
9
inspond-nuxt-safekiso/base/plugins/quill.client.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineNuxtPlugin } from '#app';
|
||||||
|
|
||||||
|
import { QuillEditor } from '@vueup/vue-quill';
|
||||||
|
|
||||||
|
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
nuxtApp.vueApp.component('QuillEditor', QuillEditor);
|
||||||
|
});
|
||||||
10
inspond-nuxt-safekiso/base/plugins/vueGoogleMaps.ts
Normal file
10
inspond-nuxt-safekiso/base/plugins/vueGoogleMaps.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import VueGoogleMaps from '@fawmi/vue-google-maps';
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const config = useRuntimeConfig().public;
|
||||||
|
nuxtApp.vueApp.use(VueGoogleMaps, {
|
||||||
|
load: {
|
||||||
|
key: config.GOOGLE_MAPS_API_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
12
inspond-nuxt-safekiso/base/plugins/vueJsonPretty.ts
Normal file
12
inspond-nuxt-safekiso/base/plugins/vueJsonPretty.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import VueJsonPretty from 'vue-json-pretty';
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((/* nuxtApp */) => {
|
||||||
|
// _utils.log('defineNuxtPlugin() of dayjs.ts executed...');
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
vueJsonPretty: () => {
|
||||||
|
return new VueJsonPretty();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
473
inspond-nuxt-safekiso/base/src/crossCtl.ts
Normal file
473
inspond-nuxt-safekiso/base/src/crossCtl.ts
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
import { _utils } from '@/base/src/utils';
|
||||||
|
|
||||||
|
import { _siteConfig } from '@/config/site';
|
||||||
|
|
||||||
|
import { ref } from '#imports';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ShieldExclamationIcon,
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
} from '@heroicons/vue/24/outline/index.js';
|
||||||
|
|
||||||
|
class CrossCtl {
|
||||||
|
// constructor() {}
|
||||||
|
|
||||||
|
siteConfig = _siteConfig;
|
||||||
|
|
||||||
|
userProfile = {};
|
||||||
|
|
||||||
|
userInfo = {};
|
||||||
|
|
||||||
|
isAuthenticated = ref(false);
|
||||||
|
|
||||||
|
isSignUpInfoNoticed = false;
|
||||||
|
|
||||||
|
profileUrlRef = ref('');
|
||||||
|
|
||||||
|
menu = ref();
|
||||||
|
|
||||||
|
// const config = useRuntimeConfig();
|
||||||
|
|
||||||
|
config = {
|
||||||
|
// API_BASE_URL: 'https://dev.twf.today/api/',
|
||||||
|
};
|
||||||
|
|
||||||
|
modalSerial = 0;
|
||||||
|
|
||||||
|
getEmptyModalInfo() {
|
||||||
|
return {
|
||||||
|
serial: -1,
|
||||||
|
icon: null,
|
||||||
|
classIcon: '',
|
||||||
|
classBg: '',
|
||||||
|
classBtnBg: '',
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
btnCount: 0,
|
||||||
|
btnTexts: [''],
|
||||||
|
onCloseCb: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
currentModalInfo = ref(this.getEmptyModalInfo());
|
||||||
|
|
||||||
|
modalQueue = [];
|
||||||
|
|
||||||
|
onModalClosed(serial, btnIdx) {
|
||||||
|
if (this.currentModalInfo.value != null) {
|
||||||
|
if (this.currentModalInfo.value['serial'] == serial) {
|
||||||
|
this.currentModalInfo.value['onCloseCb'](serial, btnIdx);
|
||||||
|
this.modalQueue.shift();
|
||||||
|
if (this.modalQueue.length > 0) {
|
||||||
|
this.currentModalInfo.value = this.modalQueue[0];
|
||||||
|
} else {
|
||||||
|
this.currentModalInfo.value = this.getEmptyModalInfo();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`modla serial missmatch current = ${this.currentModalInfo.value['serial']}, received = ${serial}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`modla callback missing. received serial = ${serial}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIconByTag(tag) {
|
||||||
|
switch (tag) {
|
||||||
|
case 'ok':
|
||||||
|
return {
|
||||||
|
icon: CheckIcon,
|
||||||
|
class: 'h-6 w-6 text-green-600',
|
||||||
|
bg: 'mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10',
|
||||||
|
btn: 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm',
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
return {
|
||||||
|
icon: ShieldExclamationIcon,
|
||||||
|
class: 'h-6 w-6 text-red-600',
|
||||||
|
bg: 'mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10',
|
||||||
|
btn: 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm',
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
icon: ExclamationCircleIcon,
|
||||||
|
class: 'h-6 w-6 text-gray-600',
|
||||||
|
bg: 'bg-gray-100',
|
||||||
|
btn: 'mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm',
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openModal(icon, title, message, btnTexts, onCloseCb) {
|
||||||
|
const newModalInfo = {
|
||||||
|
serial: this.modalSerial++,
|
||||||
|
type: icon,
|
||||||
|
icon: this.getIconByTag(icon)['icon'],
|
||||||
|
classIcon: this.getIconByTag(icon)['class'],
|
||||||
|
classBg: this.getIconByTag(icon)['bg'],
|
||||||
|
classBtnBg: this.getIconByTag(icon)['btn'],
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
btnCount: btnTexts.length,
|
||||||
|
btnTexts: btnTexts,
|
||||||
|
onCloseCb: onCloseCb,
|
||||||
|
};
|
||||||
|
this.modalQueue.push(newModalInfo);
|
||||||
|
if (this.currentModalInfo.value['serial'] == -1) {
|
||||||
|
this.currentModalInfo.value = newModalInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('newModalInfo=', newModalInfo);
|
||||||
|
return newModalInfo.serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(_config) {
|
||||||
|
this.config = _config;
|
||||||
|
this.router = useRouter();
|
||||||
|
_utils.log('config captured...', this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserInfo(info) {
|
||||||
|
this.userInfo = info;
|
||||||
|
|
||||||
|
// this.userInfo['userInfo']['userName']
|
||||||
|
|
||||||
|
// console.log('huk info = ', info);
|
||||||
|
|
||||||
|
if (!info.isAuthenticated) {
|
||||||
|
this.profileUrlRef.value = '';
|
||||||
|
} else {
|
||||||
|
this.profileUrlRef.value = info['userInfo']['profileUrl'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthInfo(infos) {
|
||||||
|
_utils.log('authInfo=', infos);
|
||||||
|
this.isAuthenticated.value = infos.isAuthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserProfile(profile) {
|
||||||
|
this.userProfile = profile;
|
||||||
|
if (
|
||||||
|
this.userInfo['isAuthenticated'] == true &&
|
||||||
|
this.userInfo['isAdmin'] == true
|
||||||
|
) {
|
||||||
|
this.menu.value = _siteConfig.menus['admin'];
|
||||||
|
} else if (
|
||||||
|
this.userInfo['isAuthenticated'] == true &&
|
||||||
|
this.userInfo['isAdmin'] == false &&
|
||||||
|
this.userInfo['isSuperOp'] == true &&
|
||||||
|
this.userInfo['isOp'] == false
|
||||||
|
) {
|
||||||
|
this.menu.value = _siteConfig.menus['super_op'];
|
||||||
|
} else if (
|
||||||
|
this.userInfo['isAuthenticated'] == true &&
|
||||||
|
this.userInfo['isAdmin'] == false &&
|
||||||
|
this.userInfo['isSuperOp'] == false &&
|
||||||
|
this.userInfo['isOp'] == true
|
||||||
|
) {
|
||||||
|
this.menu.value = _siteConfig.menus['op'];
|
||||||
|
} else if (
|
||||||
|
this.userInfo['isAuthenticated'] == true &&
|
||||||
|
this.userInfo['isAdmin'] == false &&
|
||||||
|
this.userInfo['isSuperOp'] == false &&
|
||||||
|
this.userInfo['isOp'] == false
|
||||||
|
) {
|
||||||
|
this.menu.value = _siteConfig.menus['user'];
|
||||||
|
} else {
|
||||||
|
this.menu.value = _siteConfig.menus['anonym'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrifileImageUrl() {
|
||||||
|
if (this.isAuthenticated.value == false) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return this.userInfo['profileUrl'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async doComm(cmd: string, target: string, params: any) {
|
||||||
|
let fetchOptions = {};
|
||||||
|
|
||||||
|
const newParams = { ...params };
|
||||||
|
|
||||||
|
let queryStr = '';
|
||||||
|
|
||||||
|
if (target != '') {
|
||||||
|
newParams['target'] = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'signout':
|
||||||
|
case 'local/list':
|
||||||
|
case 'local/select':
|
||||||
|
case 'list':
|
||||||
|
fetchOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
params: newParams,
|
||||||
|
};
|
||||||
|
queryStr = '?' + new URLSearchParams(newParams).toString();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fetchOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(newParams),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('fetchOptions=', fetchOptions);
|
||||||
|
|
||||||
|
fetchOptions = {
|
||||||
|
...fetchOptions,
|
||||||
|
async onRequestError({ request, options, error }) {
|
||||||
|
// Log error
|
||||||
|
console.log('[fetch request error]', request, error);
|
||||||
|
},
|
||||||
|
async onResponseError({ request, response, options }) {
|
||||||
|
// Log error
|
||||||
|
console.log(
|
||||||
|
'[fetch response error]',
|
||||||
|
request,
|
||||||
|
response.status,
|
||||||
|
response.body
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUrl = this.config['API_BASE_URL'] + cmd + queryStr;
|
||||||
|
//console.log('fetchUrl=', fetchUrl);
|
||||||
|
|
||||||
|
const data = await $fetch(fetchUrl, fetchOptions).catch(
|
||||||
|
async (error) => {
|
||||||
|
console.log('doComm error=', error);
|
||||||
|
return {
|
||||||
|
header: 'pONd-vERsion4',
|
||||||
|
responseCode: 503,
|
||||||
|
responseMessage: 'Network Error.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// console.log('data=', data);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
|
||||||
|
// 서버가 내려간 경우
|
||||||
|
// FetchError: Failed to fetch ()
|
||||||
|
|
||||||
|
// 서버 주소가 틀린 경우
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
async doUpload(target: string, params: any) {
|
||||||
|
let fetchOptions = {};
|
||||||
|
|
||||||
|
const newParams = { ...params };
|
||||||
|
|
||||||
|
const queryStr = '';
|
||||||
|
|
||||||
|
if (target != '') {
|
||||||
|
newParams['target'] = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
body: params,
|
||||||
|
};
|
||||||
|
//console.log('fetchOptions=', fetchOptions);
|
||||||
|
|
||||||
|
fetchOptions = {
|
||||||
|
...fetchOptions,
|
||||||
|
async onRequestError({ request, options, error }) {
|
||||||
|
// Log error
|
||||||
|
console.log('[fetch request error]', request, error);
|
||||||
|
},
|
||||||
|
async onResponseError({ request, response, options }) {
|
||||||
|
// Log error
|
||||||
|
console.log(
|
||||||
|
'[fetch response error]',
|
||||||
|
request,
|
||||||
|
response.status,
|
||||||
|
response.body
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUrl = this.config['API_BASE_URL'] + 'upload' + queryStr;
|
||||||
|
//console.log('fetchUrl=', fetchUrl);
|
||||||
|
|
||||||
|
const data = await $fetch(fetchUrl, fetchOptions).catch(
|
||||||
|
async (error) => {
|
||||||
|
console.log('doComm error=', error);
|
||||||
|
return {
|
||||||
|
header: 'pONd-vERsion4',
|
||||||
|
responseCode: 503,
|
||||||
|
responseMessage: 'Network Error.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// console.log('data=', data);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
|
||||||
|
// 서버가 내려간 경우
|
||||||
|
// FetchError: Failed to fetch ()
|
||||||
|
|
||||||
|
// 서버 주소가 틀린 경우
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
async doFilterRaw(key: string, params: any) {
|
||||||
|
let fetchOptions = {};
|
||||||
|
|
||||||
|
fetchOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'x-api-key': key,
|
||||||
|
},
|
||||||
|
body: new URLSearchParams(params),
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOptions = {
|
||||||
|
...fetchOptions,
|
||||||
|
async onRequestError({ request, options, error }) {
|
||||||
|
// Log error
|
||||||
|
console.log('[fetch request error]', request, error);
|
||||||
|
},
|
||||||
|
async onResponseError({ request, response, options }) {
|
||||||
|
// Log error
|
||||||
|
console.log(
|
||||||
|
'[fetch response error]',
|
||||||
|
request,
|
||||||
|
response.status,
|
||||||
|
response.body
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUrl = this.config['API_BASE_URL'] + 'v1/filter';
|
||||||
|
console.log('fetchUrl=', fetchUrl);
|
||||||
|
|
||||||
|
const data = await $fetch(fetchUrl, fetchOptions).catch(
|
||||||
|
async (error) => {
|
||||||
|
console.log('doComm error=', error);
|
||||||
|
return {
|
||||||
|
header: 'pONd-vERsion4',
|
||||||
|
responseCode: 503,
|
||||||
|
responseMessage: 'Network Error.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// console.log('data=', data);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async doFilter(keyTail: string, params: any) {
|
||||||
|
return this.doFilterRaw(
|
||||||
|
'04f4909fd242509fa03e9648236d98d8' + keyTail,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMenuItem = null;
|
||||||
|
router = null;
|
||||||
|
moveToMenuItem(item) {
|
||||||
|
if (this.lastMenuItem != null) {
|
||||||
|
this.lastMenuItem['current'] = false;
|
||||||
|
}
|
||||||
|
this.lastMenuItem = item;
|
||||||
|
item.current = true;
|
||||||
|
|
||||||
|
if (item.path != '') {
|
||||||
|
/*
|
||||||
|
this.router.push({
|
||||||
|
path: item.path,
|
||||||
|
params: {},
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
// navigateTo(item.path);
|
||||||
|
if (item.path.startsWith('/')) {
|
||||||
|
navigateTo(item.path);
|
||||||
|
} else {
|
||||||
|
window.open(item.path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('더미 메뉴');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boardInfoCacheTTL = 60 * 24 * 1; // 1 minute
|
||||||
|
boardInfoPool = {};
|
||||||
|
async getBoardInfo(routeInstance) {
|
||||||
|
const currentNow: number = Date.now();
|
||||||
|
let targetId = null;
|
||||||
|
if (routeInstance.params.boardId instanceof Array) {
|
||||||
|
if (routeInstance.params.boardId.length != 1) {
|
||||||
|
return throwError('$404');
|
||||||
|
} else {
|
||||||
|
targetId = routeInstance.params.boardId[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return throwError('$404');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('try to get boardInfo from cache for ', targetId);
|
||||||
|
let targetInfo = this.boardInfoPool[targetId];
|
||||||
|
|
||||||
|
if (targetInfo != undefined) {
|
||||||
|
const targetCacheAge: number = parseInt(
|
||||||
|
(currentNow - parseInt(targetInfo['created'])) / (1000 * 60)
|
||||||
|
);
|
||||||
|
if (true) {
|
||||||
|
// if (targetCacheAge > this.boardInfoCacheTTL) {
|
||||||
|
targetInfo = undefined;
|
||||||
|
} else {
|
||||||
|
console.log('cache hit for ', targetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetInfo == undefined) {
|
||||||
|
console.log('try to get boardInfo from server for ', targetId);
|
||||||
|
|
||||||
|
const responseJson = await this.doComm('select', 'board:info', {
|
||||||
|
hero: targetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseJson['responseCode'] == 200) {
|
||||||
|
this.boardInfoPool[targetId] = {
|
||||||
|
created: Date.now(),
|
||||||
|
info: responseJson['data'][0],
|
||||||
|
};
|
||||||
|
targetInfo = this.boardInfoPool[targetId];
|
||||||
|
} else if (responseJson['responseCode'] == 404) {
|
||||||
|
return throwError('$404');
|
||||||
|
} else if (responseJson['responseCode'] == 401) {
|
||||||
|
return throwError('$401');
|
||||||
|
} else {
|
||||||
|
return throwError('$' + responseJson['responseCode']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('do some gatekeeping works for id ', targetId);
|
||||||
|
|
||||||
|
return targetInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _crossCtl = new CrossCtl();
|
||||||
288
inspond-nuxt-safekiso/base/src/utils.ts
Normal file
288
inspond-nuxt-safekiso/base/src/utils.ts
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
function dateFormat(date, fstr, utc) {
|
||||||
|
utc = utc ? 'getUTC' : 'get';
|
||||||
|
return fstr.replace(/%[YmdHMS]/g, function (m) {
|
||||||
|
switch (m) {
|
||||||
|
case '%Y':
|
||||||
|
return date[utc + 'FullYear'](); // no leading zeros required
|
||||||
|
case '%m':
|
||||||
|
m = 1 + date[utc + 'Month']();
|
||||||
|
break;
|
||||||
|
case '%d':
|
||||||
|
m = date[utc + 'Date']();
|
||||||
|
break;
|
||||||
|
case '%H':
|
||||||
|
m = date[utc + 'Hours']();
|
||||||
|
break;
|
||||||
|
case '%M':
|
||||||
|
m = date[utc + 'Minutes']();
|
||||||
|
break;
|
||||||
|
case '%S':
|
||||||
|
m = date[utc + 'Seconds']();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return m.slice(1); // unknown code, remove %
|
||||||
|
}
|
||||||
|
// add leading zero if required
|
||||||
|
return ('0' + m).slice(-2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNow() {
|
||||||
|
return dateFormat(new Date(), '%Y-%m-%d %H:%M:%S', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (process.server) {
|
||||||
|
log('debug', 'utils in server side');
|
||||||
|
} else {
|
||||||
|
log('debug', 'utils in client side');
|
||||||
|
|
||||||
|
function getDomain(url) {
|
||||||
|
let domain = url.replace(/(https?:\/\/)?(www.)?/i, '');
|
||||||
|
if (domain.indexOf('/') !== -1) {
|
||||||
|
domain = domain.split('/')[0].toLowerCase();
|
||||||
|
}
|
||||||
|
if (domain.indexOf(':') != -1) {
|
||||||
|
const tmpAry = domain.split(':');
|
||||||
|
domain = tmpAry[0];
|
||||||
|
}
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentHost = window.location.host.toLowerCase();
|
||||||
|
const currentProtocol = window.location.protocol;
|
||||||
|
const currentDomain = getDomain(window.location.href);
|
||||||
|
|
||||||
|
console.log('currentHost = ' + currentHost);
|
||||||
|
console.log('currentProtocol = ' + currentProtocol);
|
||||||
|
console.log('currentDomain = ' + currentDomain);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Utils {
|
||||||
|
// constructor() {}
|
||||||
|
|
||||||
|
getDomain(url) {
|
||||||
|
let domain = url.replace(/(https?:\/\/)?(www.)?/i, '');
|
||||||
|
if (domain.indexOf('/') !== -1) {
|
||||||
|
domain = domain.split('/')[0].toLowerCase();
|
||||||
|
}
|
||||||
|
if (domain.indexOf(':') != -1) {
|
||||||
|
const tmpAry = domain.split(':');
|
||||||
|
domain = tmpAry[0];
|
||||||
|
}
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracer() {
|
||||||
|
try {
|
||||||
|
throw new Error();
|
||||||
|
} catch (e) {
|
||||||
|
const tmpAry = e.stack.toString().split('\n');
|
||||||
|
// console.log('tmpAry=', tmpAry);
|
||||||
|
let locTag = 'unidentified';
|
||||||
|
const callerInfo = tmpAry[2];
|
||||||
|
if (callerInfo.indexOf('/_nuxt/') != -1) {
|
||||||
|
const tmpSubAry = tmpAry[2].split('/_nuxt/');
|
||||||
|
if (tmpSubAry[tmpSubAry.length - 1].toString().endsWith(')')) {
|
||||||
|
locTag = tmpSubAry[tmpSubAry.length - 1]
|
||||||
|
.toString()
|
||||||
|
.substring(
|
||||||
|
0,
|
||||||
|
tmpSubAry[tmpSubAry.length - 1].toString().length -
|
||||||
|
1
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
locTag = tmpSubAry[tmpSubAry.length - 1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const tmpSubAry = tmpAry[2].split('/');
|
||||||
|
locTag = tmpSubAry[tmpSubAry.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.server) {
|
||||||
|
console.log(
|
||||||
|
`trace called from ${locTag}, NODE_ENV = ${process.env.NODE_ENV}, server side`
|
||||||
|
);
|
||||||
|
} else if (process.client) {
|
||||||
|
console.log(
|
||||||
|
`trace called from ${locTag}, NODE_ENV = ${process.env.NODE_ENV}, client side`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`trace called from ${locTag}, NODE_ENV = ${process.env.NODE_ENV}, unknown side`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(...args) {
|
||||||
|
let waste = 'DEBUG';
|
||||||
|
// console.log(`args.length = ${args.length}`)
|
||||||
|
|
||||||
|
if (args.length > 1) {
|
||||||
|
waste = args[0];
|
||||||
|
waste = waste.toLowerCase();
|
||||||
|
// console.log("waste = ", waste)
|
||||||
|
switch (waste) {
|
||||||
|
case 'trace':
|
||||||
|
case 'debug':
|
||||||
|
case 'info':
|
||||||
|
case 'warn':
|
||||||
|
case 'error':
|
||||||
|
case 'fatal':
|
||||||
|
waste = args.shift();
|
||||||
|
waste = waste.toUpperCase();
|
||||||
|
console.log(`[${getNow()}] ${waste} :`, ...args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`[${getNow()}] DEBUG :`, ...args);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`[${getNow()}] DEBUG :`, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(source: string) {
|
||||||
|
const entityMap = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
'/': '/',
|
||||||
|
'`': '`',
|
||||||
|
'=': '=',
|
||||||
|
};
|
||||||
|
|
||||||
|
return String(source).replace(/[&<>"'`=\/]/g, function (s) {
|
||||||
|
return entityMap[s];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
formatNumberInBytesStyle(bytes, decimals) {
|
||||||
|
if (bytes === 0) return '0';
|
||||||
|
let sign = '';
|
||||||
|
if (bytes < 0) {
|
||||||
|
sign = '-';
|
||||||
|
bytes = Math.abs(bytes);
|
||||||
|
}
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals || 2;
|
||||||
|
const sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
// log('formatBytes(), bytes, decimals, k, dm, i =', bytes, decimals, k, dm, i)
|
||||||
|
return (
|
||||||
|
sign +
|
||||||
|
parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) +
|
||||||
|
' ' +
|
||||||
|
sizes[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatBytes(bytes, decimals) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
let sign = '';
|
||||||
|
if (bytes < 0) {
|
||||||
|
sign = '-';
|
||||||
|
bytes = Math.abs(bytes);
|
||||||
|
}
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals || 2;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
// log('formatBytes(), bytes, decimals, k, dm, i =', bytes, decimals, k, dm, i)
|
||||||
|
return (
|
||||||
|
sign +
|
||||||
|
parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) +
|
||||||
|
' ' +
|
||||||
|
sizes[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatNumberWithComma(num) {
|
||||||
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
}
|
||||||
|
|
||||||
|
setCookie(name, value, days) {
|
||||||
|
let expires = '';
|
||||||
|
if (days) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||||
|
expires = '; expires=' + date.toUTCString();
|
||||||
|
}
|
||||||
|
document.cookie = name + '=' + (value || '') + expires + '; path=/';
|
||||||
|
}
|
||||||
|
|
||||||
|
getCookie(name) {
|
||||||
|
const nameEQ = name + '=';
|
||||||
|
const ca = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
|
||||||
|
if (c.indexOf(nameEQ) == 0)
|
||||||
|
return c.substring(nameEQ.length, c.length);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
rmvCookie(name) {
|
||||||
|
document.cookie = name + '=; Max-Age=-99999999;';
|
||||||
|
}
|
||||||
|
|
||||||
|
getDateTimeTag(base) {
|
||||||
|
let resultTag = '';
|
||||||
|
|
||||||
|
switch (base) {
|
||||||
|
case 'y':
|
||||||
|
resultTag = dateFormat(new Date(), '%Y', false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'm':
|
||||||
|
resultTag = dateFormat(new Date(), '%Y%m', false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'd':
|
||||||
|
resultTag = dateFormat(new Date(), '%Y%m%d', false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'h':
|
||||||
|
resultTag = dateFormat(new Date(), '%Y%m%d%H', false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'M':
|
||||||
|
resultTag = dateFormat(new Date(), '%Y%m%d%H%M', false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'boom':
|
||||||
|
resultTag = dateFormat(new Date(), '%Y%m%d%H%M', false);
|
||||||
|
resultTag = resultTag.substring(0, 11);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
resultTag = dateFormat(new Date(), '%Y-%m-%d %H:%M:%S', false);
|
||||||
|
}
|
||||||
|
return resultTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
safeJSON(data) {
|
||||||
|
let paramJSON;
|
||||||
|
try {
|
||||||
|
if (data == null) {
|
||||||
|
paramJSON = {};
|
||||||
|
} else {
|
||||||
|
paramJSON = JSON.parse(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
paramJSON = { _raw: data };
|
||||||
|
this.log('error', 'JSON parse failed data : ' + `[${data}]`);
|
||||||
|
this.log('error', 'JSON parse failed err : ' + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return paramJSON;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _utils = new Utils();
|
||||||
153
inspond-nuxt-safekiso/components/StatisticsTable1.vue
Normal file
153
inspond-nuxt-safekiso/components/StatisticsTable1.vue
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div
|
||||||
|
class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<table class="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="(heading, index) in headings"
|
||||||
|
:key="index"
|
||||||
|
:width="
|
||||||
|
heading['widthRatio'] != ''
|
||||||
|
? heading['widthRatio'] + '%'
|
||||||
|
: '100%'
|
||||||
|
"
|
||||||
|
:class="
|
||||||
|
index == 0
|
||||||
|
? 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 md:pl-0'
|
||||||
|
: 'py-3.5 px-3 text-left text-sm font-semibold text-gray-900'
|
||||||
|
"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
{{ heading['title'] }}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index + headings.length"
|
||||||
|
:width="actions.length * 1"
|
||||||
|
class="relative py-3.5 pl-3 pr-4 sm:pr-6 md:pr-0"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ action }}</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
<tr v-for="(item, itemIndex) in data" :key="itemIndex">
|
||||||
|
<td
|
||||||
|
v-for="(heading, index) in headings"
|
||||||
|
:key="index"
|
||||||
|
:class="
|
||||||
|
index == 0
|
||||||
|
? 'whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 md:pl-0'
|
||||||
|
: 'whitespace-nowrap py-4 px-3 text-sm text-gray-500'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
columnFilter
|
||||||
|
? columnFilter(
|
||||||
|
heading['key'],
|
||||||
|
item[heading['key']]
|
||||||
|
)
|
||||||
|
: item[heading['key']]
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td
|
||||||
|
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 md:pr-0"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
:class="index != 0 ? 'ml-3' : ''"
|
||||||
|
class="text-indigo-600 hover:text-indigo-900"
|
||||||
|
@click="doAction(action, item[actionKey])"
|
||||||
|
>{{ action
|
||||||
|
}}<span class="sr-only"
|
||||||
|
>, {{ item['serial'] }}</span
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="data.length == 0">
|
||||||
|
<td
|
||||||
|
:colspan="headings.length"
|
||||||
|
class="whitespace-nowrap py-4 px-3 text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
{{ noDataMessage }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-for="(heading, index) in headings"
|
||||||
|
:key="index"
|
||||||
|
:width="
|
||||||
|
heading['widthRatio'] != ''
|
||||||
|
? heading['widthRatio'] + '%'
|
||||||
|
: '100%'
|
||||||
|
"
|
||||||
|
:class="
|
||||||
|
index == 0
|
||||||
|
? 'py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 md:pl-0'
|
||||||
|
: 'py-3.5 px-3 text-left text-sm font-semibold text-gray-900'
|
||||||
|
"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
index == 0
|
||||||
|
? '합계'
|
||||||
|
: calcSum(heading['key'])
|
||||||
|
}}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index + headings.length"
|
||||||
|
:width="actions.length * 1"
|
||||||
|
class="relative py-3.5 pl-3 pr-4 sm:pr-6 md:pr-0"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ action }}</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
headings: { type: Array<object>, required: true },
|
||||||
|
actions: { type: Array<string>, default: [] },
|
||||||
|
data: { type: Array<object>, required: true },
|
||||||
|
actionKey: { type: String, default: 'serial' },
|
||||||
|
columnFilter: {
|
||||||
|
type: Function,
|
||||||
|
default: (key: string, val: string) => val,
|
||||||
|
},
|
||||||
|
doAction: {
|
||||||
|
type: Function,
|
||||||
|
default: (key: string) => {
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
noDataMessage: { type: String, default: '조회된 데이터가 없습니다.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
function calcSum(tag) {
|
||||||
|
let tmpSum = 0;
|
||||||
|
for (let i = 0; i < props.data.length; i++) {
|
||||||
|
tmpSum += props.data[i][tag];
|
||||||
|
}
|
||||||
|
return tmpSum;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
96
inspond-nuxt-safekiso/components/StickyFooter.vue
Normal file
96
inspond-nuxt-safekiso/components/StickyFooter.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="block fixed bottom-0 inset-x-0 z-50 shadow-lg text-gray-800 bg-gray-700 dark:bg-dark backdrop-blur-lg bg-opacity-30 dark:bg-opacity-30 dark:text-gray-400 border-t-2 border-royal/20"
|
||||||
|
>
|
||||||
|
<div id="tabs" class="flex justify-between">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="w-full focus:text-royal hover:text-royal justify-center inline-block text-center pt-2 pb-1 hover:bg-white"
|
||||||
|
activeClass="dark:text-gray-100 text-black"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 inline-block mb-1"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="tab block text-xs">Home</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="w-full focus:text-royal hover:text-royal justify-center inline-block text-center pt-2 pb-1 hover:bg-white"
|
||||||
|
activeClass="dark:text-gray-100 text-black"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 inline-block mb-1"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="tab block text-xs">Categories</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="w-full focus:text-royal hover:text-royal justify-center inline-block text-center pt-2 pb-1 hover:bg-white"
|
||||||
|
activeClass="dark:text-gray-100 text-black"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 inline-block mb-1"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="tab block text-xs">Gallery</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="w-full focus:text-royal hover:text-royal justify-center inline-block text-center pt-2 pb-1 hover:bg-white"
|
||||||
|
activeClass="dark:text-gray-100 text-black"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 inline-block mb-1"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="tab block text-xs">About</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
||||||
75
inspond-nuxt-safekiso/components/barChart.ts
Normal file
75
inspond-nuxt-safekiso/components/barChart.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { defineComponent, h, PropType } from 'vue';
|
||||||
|
|
||||||
|
import { Bar } from 'vue-chartjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
Plugin,
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale
|
||||||
|
);
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BarChart',
|
||||||
|
components: {
|
||||||
|
Bar,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
chartId: {
|
||||||
|
type: String,
|
||||||
|
default: 'bar-chart',
|
||||||
|
},
|
||||||
|
chartData: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
chartOptions: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 400,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 400,
|
||||||
|
},
|
||||||
|
cssClasses: {
|
||||||
|
default: '',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object as PropType<Partial<CSSStyleDeclaration>>,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
type: Array as PropType<Plugin<'bar'>[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
return () =>
|
||||||
|
h(Bar, {
|
||||||
|
chartData: props.chartData,
|
||||||
|
chartOptions: props.chartOptions,
|
||||||
|
chartId: props.chartId,
|
||||||
|
width: props.width,
|
||||||
|
height: props.height,
|
||||||
|
cssClasses: props.cssClasses,
|
||||||
|
styles: props.styles,
|
||||||
|
plugins: props.plugins,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
76
inspond-nuxt-safekiso/components/lineChart.ts
Normal file
76
inspond-nuxt-safekiso/components/lineChart.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { defineComponent, h, PropType } from 'vue';
|
||||||
|
|
||||||
|
import { Line } from 'vue-chartjs';
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
LineElement,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
CategoryScale,
|
||||||
|
Plugin,
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
LineElement,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
CategoryScale
|
||||||
|
);
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'LineChart',
|
||||||
|
components: {
|
||||||
|
Line,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
chartId: {
|
||||||
|
type: String,
|
||||||
|
default: 'line-chart',
|
||||||
|
},
|
||||||
|
chartData: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
chartOptions: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 400,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 400,
|
||||||
|
},
|
||||||
|
cssClasses: {
|
||||||
|
default: '',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object as PropType<Partial<CSSStyleDeclaration>>,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
type: Array as PropType<Plugin<'line'>[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
return () =>
|
||||||
|
h(Line, {
|
||||||
|
chartData: props.chartData,
|
||||||
|
chartOptions: props.chartOptions,
|
||||||
|
chartId: props.chartId,
|
||||||
|
width: props.width,
|
||||||
|
height: props.height,
|
||||||
|
cssClasses: props.cssClasses,
|
||||||
|
styles: props.styles,
|
||||||
|
plugins: props.plugins,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
66
inspond-nuxt-safekiso/components/pieChart.ts
Normal file
66
inspond-nuxt-safekiso/components/pieChart.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { defineComponent, h, PropType } from 'vue';
|
||||||
|
|
||||||
|
import { Pie } from 'vue-chartjs';
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ArcElement,
|
||||||
|
CategoryScale,
|
||||||
|
Plugin,
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale);
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'PieChart',
|
||||||
|
components: {
|
||||||
|
Pie,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
chartId: {
|
||||||
|
type: String,
|
||||||
|
default: 'pie-chart',
|
||||||
|
},
|
||||||
|
chartData: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
chartOptions: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 400,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 400,
|
||||||
|
},
|
||||||
|
cssClasses: {
|
||||||
|
default: '',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object as PropType<Partial<CSSStyleDeclaration>>,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
type: Array as PropType<Plugin<'pie'>[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
return () =>
|
||||||
|
h(Pie, {
|
||||||
|
chartData: props.chartData,
|
||||||
|
chartOptions: props.chartOptions,
|
||||||
|
chartId: props.chartId,
|
||||||
|
width: props.width,
|
||||||
|
height: props.height,
|
||||||
|
cssClasses: props.cssClasses,
|
||||||
|
styles: props.styles,
|
||||||
|
plugins: props.plugins,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
619
inspond-nuxt-safekiso/config/site.ts
Normal file
619
inspond-nuxt-safekiso/config/site.ts
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
import { _utils } from '@/base/src/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
InformationCircleIcon,
|
||||||
|
NewspaperIcon,
|
||||||
|
} from '@heroicons/vue/24/outline';
|
||||||
|
|
||||||
|
class siteConfig {
|
||||||
|
constructor() {
|
||||||
|
_utils.log('siteConfig Instance created...');
|
||||||
|
for (const property in this.menuItems) {
|
||||||
|
this.menuItems[property]['current'] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// siteLayout = 'top-navbar-and-footer';
|
||||||
|
// siteLayout = 'center';
|
||||||
|
// siteLayout = 'raw';
|
||||||
|
siteLayout = 'side-navbar-and-footer';
|
||||||
|
|
||||||
|
siteName = 'KISO Safeguard System';
|
||||||
|
|
||||||
|
copyrightName = '(사)한국인터넷자율정책기구';
|
||||||
|
|
||||||
|
snsLinks = [
|
||||||
|
{ tag: 'facebook', url: 'https://facebook.com/kiso.cast' },
|
||||||
|
{ tag: 'twitter', url: 'https://twitter.com/kiso_cast' },
|
||||||
|
{
|
||||||
|
tag: 'youtube',
|
||||||
|
url: 'https://www.youtube.com/channel/UCa4qy69Aqr4JqrMeBeUEtuA',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
siteSlogan = '바른 인터넷 사용 문화';
|
||||||
|
|
||||||
|
siteLogoUrl = '/inspond_logo_in_blue_bg_white.png';
|
||||||
|
|
||||||
|
menuItems = {
|
||||||
|
support_stipulation: {
|
||||||
|
title: '서비스 이용약관',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'docs-stipulation',
|
||||||
|
path: '/docs/stipulation/',
|
||||||
|
},
|
||||||
|
support_privacy: {
|
||||||
|
title: '개인정보 보호',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'docs-privacy',
|
||||||
|
path: '/docs/privacy',
|
||||||
|
},
|
||||||
|
// 이상 익명 사용자 권한
|
||||||
|
|
||||||
|
support_notice: {
|
||||||
|
title: '공지',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'support-notice',
|
||||||
|
path: '/support/notice',
|
||||||
|
},
|
||||||
|
support_faq: {
|
||||||
|
title: 'FAQ',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'support-faq',
|
||||||
|
path: '/support/faq',
|
||||||
|
},
|
||||||
|
support_inquiry: {
|
||||||
|
title: '1:1 문의',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'support-inquiry',
|
||||||
|
path: '/support/inquiry',
|
||||||
|
},
|
||||||
|
user_info: {
|
||||||
|
title: '유저 정보',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'user-info',
|
||||||
|
path: '/user/info',
|
||||||
|
},
|
||||||
|
// 이상 로그인 사용자 권한
|
||||||
|
|
||||||
|
doc_manual: {
|
||||||
|
title: '어드민 기능 안내',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'doc-manual',
|
||||||
|
path: '/doc/manual',
|
||||||
|
},
|
||||||
|
|
||||||
|
doc_guide: {
|
||||||
|
title: '서비스 도입 안내',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'doc-guide',
|
||||||
|
path: '/doc/guide',
|
||||||
|
},
|
||||||
|
|
||||||
|
doc_document: {
|
||||||
|
title: 'API 연동 안내',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'doc-api_doc',
|
||||||
|
path: '/doc/api_doc',
|
||||||
|
},
|
||||||
|
|
||||||
|
key_list: {
|
||||||
|
title: 'API 키 리스트',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'key-list',
|
||||||
|
path: '/key/list',
|
||||||
|
},
|
||||||
|
statistics: {
|
||||||
|
title: 'API 사용량 통계',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'statistics',
|
||||||
|
path: '/statistics',
|
||||||
|
},
|
||||||
|
// 이상 오퍼레이터 이상 사용 권한
|
||||||
|
|
||||||
|
filter: {
|
||||||
|
title: '필터 테스트',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'filter',
|
||||||
|
path: '/filter',
|
||||||
|
},
|
||||||
|
|
||||||
|
board_filter: {
|
||||||
|
title: '필터 관리 게시판',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'board-filter',
|
||||||
|
path: '/board/filter/list',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 이상 수퍼 오퍼레이터 이상 사용 권한
|
||||||
|
|
||||||
|
admin_dashboard: {
|
||||||
|
title: '서비스 대시보드',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-dashboard',
|
||||||
|
path: '/admin/dashboard',
|
||||||
|
},
|
||||||
|
|
||||||
|
admin_notice: {
|
||||||
|
title: '공지 관리',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-support-notice-list',
|
||||||
|
path: '/admin/support/notice/list',
|
||||||
|
},
|
||||||
|
|
||||||
|
admin_faq: {
|
||||||
|
title: '자주 묻는 질문 관리',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-support-faq-list',
|
||||||
|
path: '/admin/support/faq/list',
|
||||||
|
},
|
||||||
|
|
||||||
|
admin_inquiry: {
|
||||||
|
title: '1:1 문의 관리',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-support-inquiry-list',
|
||||||
|
path: '/admin/support/inquiry/list',
|
||||||
|
},
|
||||||
|
|
||||||
|
admin_user_list: {
|
||||||
|
title: '사용자 관리',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-user-list',
|
||||||
|
path: '/admin/user/list',
|
||||||
|
},
|
||||||
|
|
||||||
|
admin_white_list: {
|
||||||
|
title: '가입 허가 관리',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-user-white-list',
|
||||||
|
path: '/admin/user/white/list',
|
||||||
|
},
|
||||||
|
|
||||||
|
admin_board: {
|
||||||
|
title: '게시판 관리',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-board-list',
|
||||||
|
path: '/admin/board/list',
|
||||||
|
},
|
||||||
|
|
||||||
|
admin_key_list: {
|
||||||
|
title: '전체 API 키 관리',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-key-list',
|
||||||
|
path: '/admin/key/list',
|
||||||
|
},
|
||||||
|
|
||||||
|
admin_filter: {
|
||||||
|
title: '필터 관리 (admin)',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-filter',
|
||||||
|
path: '/admin/filter',
|
||||||
|
},
|
||||||
|
|
||||||
|
admin_statistics: {
|
||||||
|
title: '사용량 통계 (admin)',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-statistics',
|
||||||
|
path: '/admin/statistics',
|
||||||
|
},
|
||||||
|
|
||||||
|
admin_lab: {
|
||||||
|
title: '연구실',
|
||||||
|
icon: NewspaperIcon,
|
||||||
|
routeName: 'admin-lab',
|
||||||
|
path: '/admin/lab',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
menuIdx = 0;
|
||||||
|
|
||||||
|
menus = {
|
||||||
|
anonym: {
|
||||||
|
main: [],
|
||||||
|
sub: [
|
||||||
|
{
|
||||||
|
...this.menuItems['support_stipulation'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_privacy'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_faq'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
main: [
|
||||||
|
{
|
||||||
|
title: '고객 지원',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['support_notice'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_inquiry'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '유저',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['user_info'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sub: [
|
||||||
|
{
|
||||||
|
...this.menuItems['support_stipulation'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_privacy'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_faq'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
op: {
|
||||||
|
main: [
|
||||||
|
{
|
||||||
|
title: '고객 지원',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['support_notice'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_inquiry'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '유저',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['user_info'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'API',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['doc_guide'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['doc_document'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['doc_manual'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
...this.menuItems['key_list'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['statistics'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['filter'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sub: [
|
||||||
|
{
|
||||||
|
...this.menuItems['support_stipulation'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_privacy'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_faq'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
super_op: {
|
||||||
|
main: [
|
||||||
|
{
|
||||||
|
title: '고객 지원',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['support_notice'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_inquiry'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '유저',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['user_info'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'API',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['doc_guide'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['doc_document'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['doc_manual'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
...this.menuItems['key_list'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['statistics'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['filter'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['board_filter'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sub: [
|
||||||
|
{
|
||||||
|
...this.menuItems['support_stipulation'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_privacy'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_faq'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
admin: {
|
||||||
|
main: [
|
||||||
|
{
|
||||||
|
title: '고객 지원',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['support_notice'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_inquiry'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '유저',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['user_info'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'API',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['doc_guide'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['doc_document'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['doc_manual'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
...this.menuItems['key_list'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['statistics'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['filter'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['board_filter'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_filter'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_key_list'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_dashboard'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_lab'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_statistics'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '어드민',
|
||||||
|
icon: InformationCircleIcon,
|
||||||
|
subs: [
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_notice'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_faq'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_inquiry'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_user_list'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_white_list'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['admin_board'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sub: [
|
||||||
|
{
|
||||||
|
...this.menuItems['support_stipulation'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_privacy'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...this.menuItems['support_faq'],
|
||||||
|
idx: this.menuIdx++,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
userLevelInfo = {
|
||||||
|
'0': {
|
||||||
|
level: 0,
|
||||||
|
title: '일반 회원',
|
||||||
|
description: '서비스에 가입한 상태이나 아무런 권한이 없습니다.',
|
||||||
|
users: '표기 : user',
|
||||||
|
},
|
||||||
|
'1': {
|
||||||
|
level: 1,
|
||||||
|
title: '일반 회원',
|
||||||
|
description: '서비스에 가입한 상태이나 아무런 권한이 없습니다.',
|
||||||
|
users: '표기 : user',
|
||||||
|
},
|
||||||
|
'2': {
|
||||||
|
level: 2,
|
||||||
|
title: '일반 회원',
|
||||||
|
description: '서비스에 가입한 상태이나 아무런 권한이 없습니다.',
|
||||||
|
users: '표기 : user',
|
||||||
|
},
|
||||||
|
'3': {
|
||||||
|
level: 3,
|
||||||
|
title: '회원사 운영자',
|
||||||
|
description: '사이트 가입 후 어드민이 승인한 사용자입니다.',
|
||||||
|
users: '표기 : op',
|
||||||
|
},
|
||||||
|
'4': {
|
||||||
|
level: 4,
|
||||||
|
title: '수퍼 운영자',
|
||||||
|
description: '필터 단어 추가 권한이 부여되는 운영자 입니다.',
|
||||||
|
users: '표기 : super',
|
||||||
|
},
|
||||||
|
'5': {
|
||||||
|
level: 5,
|
||||||
|
title: '어드민',
|
||||||
|
description: '모든 기능을 사용할 수 있는 전체 서비스 관리자입니다.',
|
||||||
|
users: '표기 : admin',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
userLevels = [
|
||||||
|
this.userLevelInfo['0'],
|
||||||
|
this.userLevelInfo['3'],
|
||||||
|
this.userLevelInfo['4'],
|
||||||
|
this.userLevelInfo['5'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _siteConfig = new siteConfig();
|
||||||
230
inspond-nuxt-safekiso/content/doc/api_doc.md
Normal file
230
inspond-nuxt-safekiso/content/doc/api_doc.md
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
**(사)한국인터넷자율정책기구 KSS(KISO Safeguard System)API 서비스**
|
||||||
|
## API 연동 안내
|
||||||
|
|
||||||
|
### 개요
|
||||||
|
|
||||||
|
KISO Safeguard System API 서비스는 제시된 문자열 중에 미리 준비된 욕설·비속어 DB에 포함된
|
||||||
|
표현이 있는지를 빠르게 검사하고 그 결과를 돌려 주는 API 서비스 입니다.
|
||||||
|
|
||||||
|
- 사전 비교 방식 (2024년 4월 현재 80만여 표현)
|
||||||
|
- 아호코라식(Aho-Corasick) 알고리즘으로 빠른 검색
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 호출 주소
|
||||||
|
|
||||||
|
```log
|
||||||
|
https://www.safekiso.com/api/v1/filter
|
||||||
|
\___/ \_______________/ \____/ \____/
|
||||||
|
| | | |
|
||||||
|
protocol domain version api name
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### API 호출 파라메타
|
||||||
|
|
||||||
|
콘텐츠 타입은 application/x-www-form-urlencoded, 메쏘드는 POST로 호출 합니다.
|
||||||
|
|
||||||
|
|
||||||
|
| location | name | mandatory | Description |
|
||||||
|
|-------------|-------------|-------------|-------------|
|
||||||
|
| header | x-api-key | `required` | API 키 |
|
||||||
|
| body | text | `required` | 필터 검사 대상이 되는 문자열 |
|
||||||
|
| body | mode | `required` | 필터 모드. quick인 경우 첫번째 매칭이 발견되면 더 이상 검색하지 않고 결과값을 리턴. normal인 경우 전체를 검사하고 그 결과를 리턴. filter인 경우 전체를 검사하고 그 결과로 필터된 단어를 필터 마스크(*)로 대체한 대체 텍스트까지 결과에 포함함. |
|
||||||
|
| body | callback | `optional` | 비동기 처리 요청 응답 수신 url. 여기에 값이 설정되면 요청은 작업 큐에 저장된 후 처리 완료 후 해당 주소에 대한 호출로 결과를 리턴합니다. |
|
||||||
|
| body | checksum | `optional` | API 키를 클라이언트에서 사용하려는 경우 만료 시간을 설정하고 검증하기 위한 체크값 |
|
||||||
|
| body | ts | `optional` | 체크값 생성 근거가 되는 타임 스템프값 |
|
||||||
|
|
||||||
|
### API 호출 결과값
|
||||||
|
|
||||||
|
API 서버가 어떤 방식으로든 동작하고 있다면 서버는 HTTP(S) 호출에 대한 응답으로 반드시 200 [HTTP/1.1 - RFC2616에 명세된](https://www.ietf.org/rfc/rfc2616.txt)으로 응답합니다. 이 경우 표준 응답값과 별개로 본 서비스에서는 아래와 같은 Status.Code의 값을 이용하여 서비스의 상태와 호출 결과값을 표시합니다. 만약 표준 호출 응답이 200이 아닌 경우라면 본 서비스 자체가 아니라 로드 밸런서, 게이트 웨이 등 여러가지 레이어에서 서비스 제공이 불가능하여 응답을 보낸 것으로 판단할 수 있습니다. 이 경우에는 그 컨벤션에 맞춰 대응하시면 됩니다.
|
||||||
|
|
||||||
|
| Status.Code | Description |
|
||||||
|
|-------------|-------------|
|
||||||
|
| `2000 OK` | 모든 처리가 완료되어 정상적으로 결과를 되돌리는 경우 이 코드를 사용하게 됩니다. |
|
||||||
|
| `2020 Accepted` | 호출 파라메타중 callback에 값이 설정된 경우 실제 요청에 대한 처리를 하지 않고 일단 작업큐에 저장한 후에 이 응답이 발생합니다. 이 응답은 지정된 callback 주소를 호출할때에 응답에 함께 포함된 TrackingId의 값을 참조하여 매치할 수 있습니다. |
|
||||||
|
| `4000 Bad Request` | 어떤 이유로 지금 수신한 호출에 정상 처리가 불가능합니다. 보통의 경우 필요한 파라메타가 누락된 경우 또는 해당 파라메타에 부적절한 값이 들어가 있는 경우, 부가적인 체크섬 체크 옵션이 켜져있으나 체크에 실패한 경우 입니다. 구체적인 이유는 Status.Description의 메세지를 참고하여 조건을 수정한 후에 다시 호출해야 합니다.|
|
||||||
|
| `4010 Unauthorized` | 요청을 인증할 API 키 값이 없는 경우 발생하는 오류 입니다. `4030 Forbidden` 오류와 다른 경우임에 주의해 주세요.|
|
||||||
|
| `4030 Forbidden` | 서버에서 요청에 API 키값을 인식하였으나 해당 키가 적절한 권한을 가지지 않았다고 판정한 경우 발생합니다. 보안상의 이유로 4040과 같은 코드를 사용하지는 않는다는 점을 참고해 주세요. |
|
||||||
|
| `4290 Too Many Requests` | 아이피를 기준으로 특정 클라이언트가 너무 많은 요청을 단위시간 안에 보낸 경우에 이 응답이 리턴됩니다. 서버측에서 사용자들의 입력을 처리하는 경우에는 이 값에 주의해서 미리 큰 값으로 설정해 주어야 하며, 관리자에게 문의하여 처리가 가능합니다. |
|
||||||
|
| `5000 Internal Server Error` | 서버측의 문제로 요청에 대한 처리가 불가능한 경우 오류가 발생하였음을 알리기 위해 본 코드를 사용합니다. Description에서 보다 상세한 오류 메세지를 확인할 수 있으며 보통의 경우 이 오류는 잠시 후에 다시 시도하는 것으로 극복 가능합니다. |
|
||||||
|
| `5030 Service Unavailable` | 현재 서비스가 점검중이므로 서비스 응답할 수 없는 경우 발생합니다. |
|
||||||
|
|
||||||
|
|
||||||
|
### !!! 주의 !!!
|
||||||
|
|
||||||
|
여기에 명세된 Status.Code는 http의 호출 결과 상태 코드(http status code)와 다릅니다. 그 값을 기반으로 뒤에 0을 하나 더 붙여 만들어 진 것들로 의미를 연상하기 쉽도록 만들어 진 것이나 그대로 같은 의미가 아니고 둘은 서로 다른 값이니 참고하여 주세요. 예를 들어 Status.Code의 값이 4290인 경우에도 http status의 값은 200이 오는 것이 정상입니다. 이런 설정을 가진 이유는, API 서비스를 호출하는 과정에서 서버가 반응하지 않는 경우에는 http status의 값으로 상황을 판단하고 일단 호출 결과 http status의 값이 200이라면 이는 API 서버가 작성한 결과값이 도착했음을 의미한다고 봐도 좋습니다. http status의 값이 429가 오는 경우에는 KSS를 호스팅 하는 인프라에서 자체적으로 판단하여 호출수가 너무 많다고 판정한 경우입니다.
|
||||||
|
|
||||||
|
|
||||||
|
### API 사용예
|
||||||
|
|
||||||
|
몇가지 방법으로 예를 보이면 아래와 같습니다.
|
||||||
|
|
||||||
|
|
||||||
|
cURL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --location --request POST 'https://www.safekiso.com/api/v1/filter' \
|
||||||
|
--header 'x-api-key: <YOUR-API-KEY-HERE>' \
|
||||||
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data-urlencode 'text=검사할 문장을 이곳에 넣어 주세요.' \
|
||||||
|
--data-urlencode 'mode=filter' \
|
||||||
|
```
|
||||||
|
|
||||||
|
JavaScript - Fetch
|
||||||
|
|
||||||
|
```sh
|
||||||
|
|
||||||
|
var myHeaders = new Headers();
|
||||||
|
myHeaders.append("x-api-key", "<YOUR-API-KEY-HERE>");
|
||||||
|
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
|
||||||
|
var urlencoded = new URLSearchParams();
|
||||||
|
urlencoded.append("text", "검사할 문장을 이곳에 넣어 주세요.");
|
||||||
|
urlencoded.append("mode", "filter");
|
||||||
|
|
||||||
|
var requestOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: myHeaders,
|
||||||
|
body: urlencoded,
|
||||||
|
redirect: 'follow'
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("https://www.safekiso.com/api/v1/filter", requestOptions)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(result => console.log(result))
|
||||||
|
.catch(error => console.log('error', error));
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
NodeJs - Native
|
||||||
|
|
||||||
|
```sh
|
||||||
|
var https = require('follow-redirects').https;
|
||||||
|
var fs = require('fs');
|
||||||
|
|
||||||
|
var qs = require('querystring');
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
'method': 'POST',
|
||||||
|
'hostname': 'www.safekiso.com',
|
||||||
|
'path': '/api/v1/filter',
|
||||||
|
'headers': {
|
||||||
|
'x-api-key': '<YOUR-API-KEY-HERE>',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
'maxRedirects': 20
|
||||||
|
};
|
||||||
|
|
||||||
|
var req = https.request(options, function (res) {
|
||||||
|
var chunks = [];
|
||||||
|
|
||||||
|
res.on("data", function (chunk) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on("end", function (chunk) {
|
||||||
|
var body = Buffer.concat(chunks);
|
||||||
|
console.log(body.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on("error", function (error) {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var postData = qs.stringify({
|
||||||
|
'text': '검사할 문장을 이곳에 넣어 주세요.',
|
||||||
|
'mode': 'filter',
|
||||||
|
});
|
||||||
|
|
||||||
|
req.write(postData);
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 초당 호출수 과다 오류 발생하는 경우
|
||||||
|
응답 코드 4290이 발생하는 경우에는 [netsafe@kiso.or.kr](mailto:netsafe@kiso.or.kr)로 연락을 주셔서 키 마다 설정되는 초당 최대 호출수 설정을 변경해 주셔야 합니다. 베타 서비스 시점에서 각 API 키의 초당 최대 호출수 제한은 100으로 설정되어 있으나, 이는 사전 예고 없이 조정된 후 공지될 수 있으니 참고해 주세요. 만약 기본 값보다 더 많은 초당 호출수가 필요한 경우에는 별도의 협약이 필요하니 [netsafe@kiso.or.kr](mailto:netsafe@kiso.or.kr)로 메일 주시기 바랍니다.
|
||||||
|
|
||||||
|
|
||||||
|
### 비동기 방식으로 서비스를 호출하는 경우
|
||||||
|
|
||||||
|
callback에 콜백할 주소를 지정하면 KSS에서는 우선 다음과 같은 응답이 도착 합니다.
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"TrackingId": "7408a8f2-5843-59f5-8352-9ef2f80d89ec",
|
||||||
|
"Status": {
|
||||||
|
"Code": 2020,
|
||||||
|
"Message": "Accepted",
|
||||||
|
"Description": "Filter work created with trackingId 7408a8f2-5843-59f5-8352-9ef2f80d89ec"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
실제 주어진 문장에 대한 필터 작업이 끝난 후에는 지정된 웹 주소로 다음과 같은 코드가 동작 합니다.
|
||||||
|
```
|
||||||
|
let options = {
|
||||||
|
uri: req.body.callback,
|
||||||
|
method: 'POST',
|
||||||
|
body: response,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
request.post(options, function (error, response, body) {
|
||||||
|
localHandler.handleApiFinalResponse(req, response, {
|
||||||
|
error: error,
|
||||||
|
response: response,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
응답(response)의 예는 아래와 같습니다.
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"TrackingId": "7408a8f2-5843-59f5-8352-9ef2f80d89ec",
|
||||||
|
"Status": {
|
||||||
|
"Code": 2000,
|
||||||
|
"Message": "OK",
|
||||||
|
"Description": ""
|
||||||
|
},
|
||||||
|
"Detected": [
|
||||||
|
[
|
||||||
|
3,
|
||||||
|
"개자식"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"Filtered": "이런 ***을 봤나.",
|
||||||
|
"Elapsed": "0 s, 10.448 ms"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 클라이언트측에서 API 키를 사용하려는 경우
|
||||||
|
|
||||||
|
이 경우 먼저 [netsafe@kiso.or.kr](mailto:netsafe@kiso.or.kr) 메일로 사용하실 키를 알려 주시고 저희가 기반 설정을 마친 후에 구체적인 검증값 생성 방법과 테스트를 도와 드립니다. 이 방식에 대한 상세한 안내는 보안과 관련이 있으므로 부득이 메일로 개별 안내 하는 것에 대해 양해 부탁 드립니다.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 기타 API 서비스 이용 안내
|
||||||
|
|
||||||
|
<a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/doc/guide" >서비스 이용 안내</a> 를 참고해 주세요.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### License
|
||||||
|
Copyright © 2023 [(사)한국인터넷자율정책기구](https://www.kiso.or.kr/)
|
||||||
|
|
||||||
23
inspond-nuxt-safekiso/content/doc/bill.md
Normal file
23
inspond-nuxt-safekiso/content/doc/bill.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
**(사)한국인터넷자율정책기구 KSS(KISO Safeguard System)API 서비스**
|
||||||
|
## 서비스 이용 요금
|
||||||
|
|
||||||
|
### 기본 설정
|
||||||
|
|
||||||
|
KISO Safeguard System API 서비스는 KISO 회원사, 관공서, 언론사에는 무료로 제공되며 그 외에는 월 6만원의 사용료가 부과되고 사용량의 제한은 다음과 같습니다.
|
||||||
|
|
||||||
|
| 항목 | 제한 값 |
|
||||||
|
|--------------------|-------------|
|
||||||
|
| 초당 호출 수 | 100회 (하나의 API 키 마다 제한) |
|
||||||
|
| 일 최대 호출 수 | 8,640,000회 |
|
||||||
|
| 계정 당 키 생성 제한 | 5개 |
|
||||||
|
|
||||||
|
|
||||||
|
단, 이 내용은 내부 사정에 따라 향후 변경될 수 있음을 알려 드립니다.
|
||||||
|
|
||||||
|
계정당 키 생성 제한이나 초당 호출 제한 등은 시스템을 함께 사용하는 다른 고객사에 문제가 생기지 않도록 하기 위한 최소한의 제한이며 실제로 허용된 최대 사용량을 사용하는 경우에는 전체 시스템의 안정성을 위하여 별도의 협약이 필요할 수 있습니다.
|
||||||
|
|
||||||
|
또한 위 사용량 제한보다 더 많은 호출이 필요한 경우에는 별도의 논의와 협의를 거쳐 서비스를 제공하는 방법이 마련되어 있으니 문의 바랍니다.
|
||||||
|
### License
|
||||||
|
|
||||||
|
(사)한국인터넷자율정책기구
|
||||||
|
|
||||||
93
inspond-nuxt-safekiso/content/doc/certification.md
Normal file
93
inspond-nuxt-safekiso/content/doc/certification.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
**(사)한국인터넷자율정책기구 KSS(KISO Safeguard System)API 서비스**
|
||||||
|
## 인증 로고 안내
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
KSS API를 이용하는 서비스에서 필터된 결과를 사용자에게 표시할 때에 혹은 필터가 될 것임을 안내하는 경우에 활용하실 수 있도록 인증 로고를 준비 하였습니다. 인증 로고는 다음과 같은 효과를 기대하고 만들어 졌습니다.
|
||||||
|
|
||||||
|
- 사용자 입력에 대한 표준적인 대응을 구현하였음을 사전에 표시하여 서비스 신뢰도 향상
|
||||||
|
- 사용자들에게 필터 DB의 관리 주체를 명시함으로써 운영의 편의성과 객관성을 확보
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 준비된 내용
|
||||||
|
|
||||||
|
- 로고의 제작 의도 [1](/logos/description/kss_certification_logo_01.jpg){:target="_blank"} [2](/logos/description/kss_certification_logo_02.jpg){:target="_blank"} [3](/logos/description/kss_certification_logo_03.jpg){:target="_blank"} [4](/logos/description/kss_certification_logo_04.jpg){:target="_blank"}
|
||||||
|
- 직사각 기본 로고 : [영문 컬러 버전](/logos/kss_certification_logo_box_english.png){:target="_blank"}, [영문 흑백 버전](/logos/kss_certification_logo_box_english_g.png){:target="_blank"}, [한글 컬러 버전](/logos/kss_certification_logo_box_korean.png){:target="_blank"}, [한글 흑백 버전](/logos/kss_certification_logo_box_korean_g.png){:target="_blank"} (png 포맷)
|
||||||
|
- 간단 심볼 : [영문 컬러 버전](/logos/kss_certification_logo_symbol.png){:target="_blank"}, [영문 흑백 버전](/logos/kss_certification_logo_symbol_g.png){:target="_blank"} (png 포맷)
|
||||||
|
- 정사각 로고 : [영문 컬러 버전](/logos/kss_certification_logo_variation_english.png){:target="_blank"}, [영문 흑백 버전](/logos/kss_certification_logo_variation_english_g.png){:target="_blank"}, [한글 컬러 버전](/logos/kss_certification_logo_variation_korean.png){:target="_blank"}, [한글 흑백 버전](/logos/kss_certification_logo_variation_korean_g.png){:target="_blank"} (png 포맷)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 사용 예
|
||||||
|
|
||||||
|
인증 로고는 위에 링크로부터 직접 다운로드 받아서 활용하실 수 도 있고, 아래와 같이 HTML태그로 KSS 서버로부터 불러 사용하실 수도 있습니다.
|
||||||
|
|
||||||
|
아래는 인증 로고를 클릭 하는 경우 설명 페이지로 이동하는 태그까지 포함한 예 입니다.
|
||||||
|
|
||||||
|
|
||||||
|
``` html
|
||||||
|
<a href="https://www.safekiso.com/doc/kss" rel="nofollow" title="KSS API Site Certification Logo"><img src="https://www.safekiso.com/logos/kss_certification_logo_box_english.png" width="243" height="66" title="KSS API로 보호되고 있는 사이트 입니다." alt="KSS API Site Certification Logo"/></a>
|
||||||
|
```
|
||||||
|
|
||||||
|
위와 같은 코드는 아래와 같은 결과로 표시 됩니다.
|
||||||
|
|
||||||
|
<a href="https://www.safekiso.com/doc/kss" rel="nofollow" title="KSS API Site Certification Logo"><img src="https://www.safekiso.com/logos/kss_certification_logo_box_english.png" width="243" height="66" title="KSS API로 보호되고 있는 사이트 입니다." alt="KSS API Site Certification Logo"/></a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
아래 코드는 영문 흑백 버전의 경우를 보여 줍니다.
|
||||||
|
|
||||||
|
|
||||||
|
``` html
|
||||||
|
<a href="https://www.safekiso.com/doc/kss" rel="nofollow" title="KSS API Site Certification Logo"><img src="https://www.safekiso.com/logos/kss_certification_logo_box_english_g.png" width="243" height="66" title="KSS API로 보호되고 있는 사이트 입니다." alt="KSS API Site Certification Logo"/></a>
|
||||||
|
```
|
||||||
|
|
||||||
|
위와 같은 코드는 아래와 같은 결과로 표시 됩니다.
|
||||||
|
|
||||||
|
<a href="https://www.safekiso.com/doc/kss" rel="nofollow" title="KSS API Site Certification Logo"><img src="https://www.safekiso.com/logos/kss_certification_logo_box_english_g.png" width="243" height="66" title="KSS API로 보호되고 있는 사이트 입니다." alt="KSS API Site Certification Logo"/></a>
|
||||||
|
|
||||||
|
|
||||||
|
아래 코드는 심볼만을 표시하는 경우를 보여 줍니다.
|
||||||
|
|
||||||
|
|
||||||
|
``` html
|
||||||
|
<a href="https://www.safekiso.com/doc/kss" rel="nofollow" title="KSS API Site Certification Logo"><img src="https://www.safekiso.com/logos/kss_certification_logo_symbol.png" width="62" height="72" title="KSS API로 보호되고 있는 사이트 입니다." alt="KSS API Site Certification Logo"/></a>
|
||||||
|
```
|
||||||
|
|
||||||
|
위와 같은 코드는 아래와 같은 결과로 표시 됩니다.
|
||||||
|
|
||||||
|
<a href="https://www.safekiso.com/doc/kss" rel="nofollow" title="KSS API Site Certification Logo"><img src="https://www.safekiso.com/logos/kss_certification_logo_symbol.png" width="62" height="72" title="KSS API로 보호되고 있는 사이트 입니다." alt="KSS API Site Certification Logo"/></a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
아래 코드는 사각형 영역의 우측 하단에 로고를 표시하는 경우를 보여 줍니다.
|
||||||
|
|
||||||
|
|
||||||
|
``` html
|
||||||
|
<a href="https://www.safekiso.com/doc/kss" rel="nofollow" title="KSS API Site Certification Logo"><img src="https://www.safekiso.com/logos/kss_certification_logo_variation_english.png" width="143" height="105" title="KSS API로 보호되고 있는 사이트 입니다." alt="KSS API Site Certification Logo"/></a>
|
||||||
|
```
|
||||||
|
|
||||||
|
위와 같은 코드는 아래와 같은 결과로 표시 됩니다.
|
||||||
|
|
||||||
|
|
||||||
|
<a href="https://www.safekiso.com/doc/kss" rel="nofollow" title="KSS API Site Certification Logo"><img align="right" src="https://www.safekiso.com/logos/kss_certification_logo_variation_english.png" width="143" height="105" title="KSS API로 보호되고 있는 사이트 입니다." alt="KSS API Site Certification Logo"/></a>
|
||||||
|
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
|
||||||
|
### 주의 사항
|
||||||
|
|
||||||
|
인증 로고를 서비스에 활용하시는 경우 KSS 사이트로부터 직접 링크하는 경우 파일의 위치가 변경되는 경우 본 서비스에 이미지가 제대로 표시되지 않는 등 부작용이 발생할 수 있으므로 가급적이면 직접 링크가 아닌 다운로드 하여 안전한 위치에서 표시, 사용 하시는 것을 권장 합니다.
|
||||||
|
|
||||||
|
|
||||||
|
### 연락처
|
||||||
|
서비스 사용 신청은 [netsafe@kiso.or.kr](mailto:netsafe@kiso.or.kr) 주소로 간단한 업체 소개와 사용 용도 등을 적어 보내 주세요. 사용 신청 전, 사용 중에 기술적인 문의 등에 대해서도 같은 메일로 문의해 주시면 답변을 받아 보실 수 있습니다.
|
||||||
|
|
||||||
|
|
||||||
|
### License
|
||||||
|
Copyright © 2023 [(사)한국인터넷자율정책기구](https://www.kiso.or.kr/)
|
||||||
|
|
||||||
30
inspond-nuxt-safekiso/content/doc/contract.md
Normal file
30
inspond-nuxt-safekiso/content/doc/contract.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
**(사)한국인터넷자율정책기구 KSS(KISO Safeguard System)API 서비스**
|
||||||
|
## 환영합니다!
|
||||||
|
|
||||||
|
### 서비스 이용 신청 안내
|
||||||
|
|
||||||
|
KISO 이용자보호시스템(KSS, KISO Safeguard System) API는 국내 대표 포털 네이버와 카카오로부터 제공받은 욕설·비속어 DB를 활용해 개발되었습니다.
|
||||||
|
|
||||||
|
해당 API는 약 60만 건의 욕설·비속어 DB를 활용해 입력된 표현 중 사전에 포함된 단어가 있는지 검사하고 그 결과를 필터링해 주는 서비스를 제공합니다.
|
||||||
|
|
||||||
|
본 서비스의 목적은 다양한 인터넷 서비스에서 사업자가 별도의 욕설·비속어 DB구축 및 유지 보수의 부담이 없이 욕설·비속어 표현에 대한 서비스 관리 및 운영에 도움을 드리는 것입니다.
|
||||||
|
|
||||||
|
|
||||||
|
본 서비스를 이용하기 위해서는 먼저 한국인터넷자율정책기구의 사전 승인이 필요합니다.
|
||||||
|
|
||||||
|
아래 이메일 주소로 간단한 소개와 서비스 사용 목적 등을 알려 주시면 내부 검토 후 이후 절차를 안내 해 드리겠습니다.
|
||||||
|
|
||||||
|
문의 : netsafe@kiso.or.kr
|
||||||
|
|
||||||
|
|
||||||
|
### 추가 정보
|
||||||
|
|
||||||
|
KSS에 대해 조금 더 알고 싶으시다면 아래 링크를 참고해 주세요
|
||||||
|
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/">KSS 소개</a>
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/doc/guide">서비스 도입 안내</a>
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/doc/api_doc">API 연동 안내</a>
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/doc/certification">인증 로고 안내</a>
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/docs/stipulation">서비스 이용 약관</a>
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/docs/privacy">개인정보 보호 정책</a>
|
||||||
|
|
||||||
100
inspond-nuxt-safekiso/content/doc/guide.md
Normal file
100
inspond-nuxt-safekiso/content/doc/guide.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
**(사)한국인터넷자율정책기구 KSS(KISO Safeguard System)API 서비스**
|
||||||
|
## 서비스 도입 안내
|
||||||
|
|
||||||
|
### 첫번째 고려 사항
|
||||||
|
모든 IT 서비스들은 24시간 365일 정상 동작하는 것을 목표로 하지만 때로는 그렇지 못한 경우가 있습니다. KSS API 서비스를 여러분의 서비스에 이용하시려는 경우 가장 먼저 생각해야 하는 것은 언제라도 본 서비스가 동작하지 않을 수 있다는 점입니다. KSS의 모든 기술 스텝들은 최선을 다하여 시스템을 모니터링 하고 유지 하겠지만, 돌발 상황이 발생하여 시스템이 동작을 멈추는 경우라도 여러분의 서비스를 이용하는 사용자들에게는 불친절한 모습을 보이지 않도록 최대한 세심하게 도입 설계를 해 주세요. 짧게 말씀 드리자면, KSS가 응답하지 않는다고 해서 여러분의 서비스가 멈추도록 해서는 안된다는 의미 입니다. 이하의 내용은 그와 관련된 몇 가지 안내와 의견입니다.
|
||||||
|
|
||||||
|
### 서비스 가입 계정의 선택
|
||||||
|
이 문서를 둘러 보시고 서비스 이용을 해 보려 하시는 경우 가입에 사용하는 이메일은 담당자 개인 메일이 아닌 조직의 역할 이메일((예를 들면 admin@ 혹은 infra@와 같은)을 사용하시는 것을 권합니다. 그리고 로그인에 사용하는 메일 계정으로 수신되는 메일을 자주 모니터링 하지 않는 경우에는 서비스로부터의 중요한 공지 사항등을 수신할 수 있도록 서비스 내에 개인 정보 수정 항목에서 이메일 주소를 늘 모니터링 되는 이 메일 주소로 변경해 주세요.
|
||||||
|
|
||||||
|
|
||||||
|
### 인증 로고 활용의 검토
|
||||||
|
KSS를 활용하여 얻을 수 있는 장점으로 표준화된 필터와 기준의 적용을 사용자들에게 안내 하여 서비스에 대한 신뢰를 높일 수 있다는 점이 가장 크다고 생각합니다. 여러분이 본 서비스를 활용하시려는 경우에는
|
||||||
|
<a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/doc/certification" >인증 로고 안내</a>
|
||||||
|
의 내용도 참고해 주세요.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 가장 일반적인 사용의 예
|
||||||
|
웹 페이지에서 DB에 게시물을 작성하는 경우를 예로 들어 보겠습니다. 사용자측에서 작성된 내용을 DB에 저장해 달라는 요청이 들어오면 서버에서는 먼저 해당 계정이 적절한 권한을 가지고 있는지 등을 검사하고 만약 조건에 맞지 않는 요청이라면 오류 메세지를 리턴합니다. 이 과정(여러가지 권한 검사)에 KSS API 호출 과정도 추가하여 만약 검사 결과가 적절치 못하다면 이런 표현은 달리 바꿔 달라는 오류 메세지로 사용자에게 되돌려 줄 수 있습니다. 만약 특정 시점에 KSS API가 정상 동작하지 않는다면(타임아웃이 될 동안 응답이 오지 않거나 서버가 아예 응답이 없거나 등) 그 사실을 그대로 사용자에게 오류 메세지로 표시하는 결과를 되돌릴 수 있습니다.
|
||||||
|
|
||||||
|
>본문 검사 과정에서 오류가 발생하였습니다. 잠시 후에 다시 시도해 주세요.
|
||||||
|
|
||||||
|
이 경우, KSS API가 정상 동작하지 않는다면 게시물의 작성이나 댓글 등이 모두 멈춘다는 단점은 있으나 구현이 가장 단순하고 사용자에게도 혼란이 가장 적은 일반적인 적용 예라 할 수 있습니다.
|
||||||
|
|
||||||
|
|
||||||
|
### 실시간 채팅, 메세징에서 사용하는 경우
|
||||||
|
실시간 채팅이나 메세지도 결국은 중계를 위해 서버를 거치게 되는데 이 과정에서 메세지를 보낸 사용자의 자격이나 계정 등에 대해 기본적인 검사를 하게 됩니다. 이 부분에 필터 API의 호출 부분을 넣어 그 결과에 따라 필터를 적용한 메세지로 바꿔 전달하거나 혹은 전송 요청을 한 사용자에게 전송 거부의 오류 메세지를 되돌려 줄 수 있습니다. 예를 든다면
|
||||||
|
|
||||||
|
>이런 개자식을 봤나.
|
||||||
|
|
||||||
|
이런 입력은
|
||||||
|
|
||||||
|
>이런 ***을 봤나.
|
||||||
|
|
||||||
|
혹은 오류 메세지로 전송을 거부할 수 있습니다.
|
||||||
|
|
||||||
|
>부적절한 표현들이 포함되어 있습니다. '개자식'
|
||||||
|
|
||||||
|
이 경우 서버측의 회신 메세지 예는 아래와 같고, 응답시간은 33밀리초 입니다.
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"TrackingId": "7408a8f2-5843-59f5-8352-9ef2f80d89ec",
|
||||||
|
"Status": {
|
||||||
|
"Code": 2000,
|
||||||
|
"Message": "OK",
|
||||||
|
"Description": ""
|
||||||
|
},
|
||||||
|
"Detected": [
|
||||||
|
[
|
||||||
|
3,
|
||||||
|
"개자식"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"Filtered": "이런 ***을 봤나.",
|
||||||
|
"Elapsed": "0 s, 10.448 ms"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### API 키를 클라이언트에서 사용하는 경우
|
||||||
|
일반적인 상황이라면 API 키는 서버측에서 사용하고 사용자측, 클라이언트측에 노출되지 않도로고 하는 것이 최선입니다. 하지만 부득이하게 클라이언트측에서 사용해야 하는 경우라면 일정 시간마다 해시값을 발생시키고 이를 API 키 호출에 추가하여 서버측에서 이 값을 검사하고 만약 복사된 키가 계속 사용된다면 오류를 되돌리도록 할 수 있습니다. 구체적인 해시 키 사용과 설정의 방법은
|
||||||
|
<a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/doc/api_doc" >API 연동 안내</a>
|
||||||
|
문서를 참고해 주세요. 단, 이 기능은 바로 사용하실 수 없으며 저희 기술팀에 문의하여 키를 이 방식 처리에 대응하도록 설정해야 합니다. 관련된 문의는 [netsafe@kiso.or.kr](mailto:netsafe@kiso.or.kr)로 메일 주세요.
|
||||||
|
|
||||||
|
### KSS가 멈춘 동안 작성된 필터 되지 않은 컨텐츠의 처리
|
||||||
|
사용자가 클라이언트측에서 무언가를 입력하고 전송 버튼을 누른 경우, 게시물의 댓글을 예로 들어 보겠습니다. 댓글은 우선 서버에서 KSS API 서비스를 호출하여 부적절한 표현이 포함되어 있는지 검사하고 만약 검사 결과가 좋지 않다면 사용자에게 이 내용은 DB에 입력할 수 없다는 오류 메세지를 출력하는 것이 일반적인 흐름입니다. 그런데 만약 KSS API가 응답이 없다면 어떻게 할까요? 어떤 댓글은 모욕적인 표현이 없는 상태로 저장되었지만 어떤 운 좋은 댓글들은 모욕적인 표현들이 그대로 포함된 채로 게시될 것입니다. 이렇게 만에 하나 발생할 수 있는 간헐적인 장애로 모욕적인 표현의 필터가 동작하거나 하지 않을 수 있는 불일치가 서비스 품질에 심각한 영향을 준다면 조금 작업이 복잡해 지긴 하지만 안전 장치를 할 수 있습니다.
|
||||||
|
|
||||||
|
게시물의 댓글 DB에 현재 이 댓글이 필터가 완료된 상태인지 필터 전인지를 표시하는 플래그와 DB에 저장된 시간(이 값은 보통 이미 있겠지요)을 저장합니다. 그리고 욕설·비속어 필터가 통과되지 않은 댓글은 '처리중'이라는 텍스트가 대신 표출되도록 해 둡니다. 댓글을 저장할 때 KSS API를 비동기 방식으로 호출할 수 있으며 처리가 끝난 후 호출될 콜백 주소를 지정할 수 있습니다. 이곳에서 해당 댓글의 검사 결과가 깨끗하다면 필터 완료로 마크하고 사용자들에게 댓글은 정상 표출될 것입니다. 만약 특정 시점에 KSS API의 동작이 실패하여 일정 시간(보통 5초 이내)이 지났음에도 필터가 완료 마크되지 않은 항목들은 한번 더 KSS 검사를 동기 방식으로 요청하여 직접 그 결과를 처리할 수 있습니다. 이런 시나리오에 필요한 세부적인 API 호출 파라메타와 예시는 <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/doc/api_doc" >API 연동 안내</a> 문서를 참고해 주세요.
|
||||||
|
|
||||||
|
|
||||||
|
### DB를 직접 활용하거나 인공지능을 이용해 욕설·비속어을 검출하는 방식에 대한 의견
|
||||||
|
KSS API 서비스는 국내 대형 포털에서 오랜 기간 고객 응대를 하며 축적된 대량의 욕설·비속어 단어 DB를 기반으로 합니다. 어떤 경우에, 사업적인 판단에 의해, 여러분의 서비스에 자체적인 욕설·비속어 DB를 구축하고 유지하는 것에 대해 검토하실 수 있습니다. 저희가 본 서비스를 구축하게 된 계기도 바로 그 지점에 대한 고민이었습니다. 우선 현실적으로 한글로 된 적절한 DB를 최초 획득하는 것이 쉽지 않습니다. 어찌 하여 꽤 괜찮은 DB를 구했다 하더라도 새로운 표현이 계속 만들어 지기 때문에 결국은 이 시스템으로 일손을 던다기 보다는 그 DB를 계속 최신으로 유지하기 위한 노력이 시스템 구축의 비용보다 장기적으로는 더 많이 든다고 생각하게 되었습니다. DB 비교 방식의 장점은 특정 이슈에 즉각적으로 대응할 수 있다는 것입니다. 우리는 그 장점과 거대한 DB를 항상 최신으로 유지하며 표준적인 가이드를 제시하는 역할을 누군가 해 준다면 회원사들이 조금 더 서비스 본연에만 집중할 수 있지 않을까 생각 했습니다. 인공지능으로 전혀 새로운, 욕설·비속어을 발견하는 것이 가능은 합니다. 하지만 결국 사용자의 컴플레인으로 그 판정들을 다시 리뷰하고 그 결과를 인공지능 엔진에 적용하는 작업이 간단하지 않습니다. 결국 욕설·비속어에 대한 필터링은 고객 응대 이슈와 끊임 없이 맞물리게 되므로, 저희가 표준화된 가이드를 제공하고 회원사들이 그것을 이용하는 것이 각자 역할의 분담이 될 것이라고 생각 합니다.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 개인 정보와 관련된 이슈
|
||||||
|
KSS API 서비스를 사용하기 위해서는 이메일 주소로 가입이 필요하며 저희가 수집하는 개인정보는 이메일 주소 뿐입니다. 나머지 항목들은 필수적인 항목이 아니며, 운영자가 사용자를 식별하기 위해 메모의 형식으로 별도 보관하는 항목들은 다음과 같습니다.
|
||||||
|
|
||||||
|
> 업체명, 담당자 이름, 직함
|
||||||
|
|
||||||
|
API 서비스를 호출하게 되면 서버는 주어진 문장을 검사하여 필터 결과만을 리턴하며 후에 통계를 생성할 목적으로 다음과 같은 내용의 로그를 남깁니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
TrackingId: 호출 고유 아이디,
|
||||||
|
uid: API 키 소유자 내부 아이디(이메일 주소 아님),
|
||||||
|
key: API 키,
|
||||||
|
hit: 검출된 단어 수,
|
||||||
|
words: 검출된 단어들,
|
||||||
|
size: 전송된 원문의 크기,
|
||||||
|
referrer: 웹 요청 리퍼럴,
|
||||||
|
ip: 웹 요청 아이피,
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 기타 문의
|
||||||
|
본 서비스와 관련해 궁금하신 점이 있다면 [netsafe@kiso.or.kr](mailto:netsafe@kiso.or.kr)로 메일 주세요.
|
||||||
36
inspond-nuxt-safekiso/content/doc/manual.md
Normal file
36
inspond-nuxt-safekiso/content/doc/manual.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
**(사)한국인터넷자율정책기구 KSS(KISO Safeguard System)API 서비스**
|
||||||
|
## 웹 어드민 기능 안내
|
||||||
|
|
||||||
|
KSS API를 사용하는데 필요한 어드민 기능을 제공 합니다. 아래와 같은 기능들을 이용하실 수 있습니다.
|
||||||
|
|
||||||
|
|
||||||
|
### 서비스 로그인 관련 기능
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/user/info">회원 정보 수정 / 회원 탈퇴</a>
|
||||||
|
- 회원 정보를 수정하거나 탈퇴 기능을 수행할 수 있습니다.
|
||||||
|
|
||||||
|
|
||||||
|
### API 키 관리 관련 기능
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/key/list">API 키 리스트</a>
|
||||||
|
- 내가 만든 API 키 리스트를 볼 수 있습니다. 리스트에 원하는 항목의 <span class="font-semibold text-indigo-600 hover:text-indigo-500">상세 보기</span> 링크를 선택 하시면 상세 정보를 보고 관리할 수 있습니다.
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/key/deleted">삭제된 API 키 리스트</a>
|
||||||
|
- 삭제된 키 리스트를 볼 수 있고 복구할 수도 있습니다. 리스트에 원하는 항목의 <span class="font-semibold text-indigo-600 hover:text-indigo-500">상세 보기</span> 링크를 선택 하세요.
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/key/new">새로운 키 생성</a>
|
||||||
|
- 키의 이름은 내가 보유한 여러 키를 식별하기 위해 입력하는 것입니다.
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/filter">필터 테스트</a>
|
||||||
|
- 어떤 문장을 입력하고 필터를 수행하여 어떤 결과가 나오는지를 미리 볼 수 있습니다.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### API 키 사용 통계 관련 기능
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/statistics">API 사용량 통계</a>
|
||||||
|
- 내가 보유한 모든 키의 사용량을 합한 통계를 볼 수 있습니다. 개별 키의 사용량은 개별 키 항목의 <span class="font-semibold text-indigo-600 hover:text-indigo-500">상세 보기</span> 링크를 선택 하신 후 우측 상단 버튼을 선택하면 보실 수 있습니다.
|
||||||
|
|
||||||
|
|
||||||
|
### 기타 기능
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/support/notice">공지 사항</a>
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/support/faq">자주 묻는 질문과 답변</a>
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/support/inquiry">1:1 문의</a>
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/doc/certification">인증 로고 안내</a>
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/docs/stipulation">서비스 이용 약관</a>
|
||||||
|
- <a class="font-semibold text-indigo-600 hover:text-indigo-500" href="/docs/privacy">개인정보 보호 정책</a>
|
||||||
|
|
||||||
2
inspond-nuxt-safekiso/dotEnv.dev
Normal file
2
inspond-nuxt-safekiso/dotEnv.dev
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
API_BASE_URL: https://www.safekiso.com/api/
|
||||||
|
GOOGLE_MAPS_API_KEY: AIzaSyDJQXJrwk2oHsNdR_QBxv9ltk3JRt4wfrc
|
||||||
120
inspond-nuxt-safekiso/error.vue
Normal file
120
inspond-nuxt-safekiso/error.vue
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
This example requires updating your template:
|
||||||
|
|
||||||
|
```
|
||||||
|
<html class="h-full">
|
||||||
|
<body class="h-full">
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center h-screen">
|
||||||
|
<main class="sm:flex">
|
||||||
|
<p class="text-4xl font-extrabold text-indigo-600 sm:text-5xl">
|
||||||
|
{{ filteredCode }}
|
||||||
|
</p>
|
||||||
|
<div class="sm:ml-6">
|
||||||
|
<div class="sm:border-l sm:border-gray-200 sm:pl-6">
|
||||||
|
<h1
|
||||||
|
class="text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl"
|
||||||
|
>
|
||||||
|
{{ filteredTitle }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-base text-gray-500">
|
||||||
|
{{ filteredMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mt-10 flex space-x-3 sm:border-l sm:border-transparent sm:pl-6"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
@click="handleError('/')"
|
||||||
|
>
|
||||||
|
Go back home
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="isAuthenticated"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
class="ml-3 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
@click="handleError('/support/inquiry')"
|
||||||
|
>
|
||||||
|
Contact support
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
error: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredCode = ref(0);
|
||||||
|
const filteredTitle = ref('');
|
||||||
|
const filteredMessage = ref('');
|
||||||
|
|
||||||
|
const isAuthenticated = ref(_crossCtl.isAuthenticated);
|
||||||
|
|
||||||
|
switch (props.error.toString()) {
|
||||||
|
case 'Error: $400':
|
||||||
|
filteredCode.value = 400;
|
||||||
|
filteredTitle.value = 'Bad Request';
|
||||||
|
filteredMessage.value =
|
||||||
|
'조건을 만족하지 않아 처리할 수 없습니다. 잠시 후 다시 시도해 주세요.';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Error: #401':
|
||||||
|
case 'Error: $401':
|
||||||
|
filteredCode.value = 401;
|
||||||
|
filteredTitle.value = 'Unauthorized';
|
||||||
|
filteredMessage.value = '로그인이 필요합니다.';
|
||||||
|
break;
|
||||||
|
case 'Error: #403':
|
||||||
|
case 'Error: $403':
|
||||||
|
filteredCode.value = 403;
|
||||||
|
filteredTitle.value = 'Forbidden';
|
||||||
|
filteredMessage.value = '권한이 필요 합니다.';
|
||||||
|
break;
|
||||||
|
case 'Error: #404':
|
||||||
|
case 'Error: $404':
|
||||||
|
filteredCode.value = 404;
|
||||||
|
filteredTitle.value = 'Not Found';
|
||||||
|
filteredMessage.value =
|
||||||
|
'없는 페이지 주소입니다. 잠시 후 다시 시도해 주세요.';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Error: #503':
|
||||||
|
filteredCode.value = 503;
|
||||||
|
filteredTitle.value = 'Network Error';
|
||||||
|
filteredMessage.value =
|
||||||
|
'서버와 통신할 수 없습니다. 인터넷 연결을 확인하시고 잠시 후 다시 시도해 주세요.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.error.message.startsWith('Page not found: ')) {
|
||||||
|
filteredCode.value = 404;
|
||||||
|
filteredTitle.value = 'Not Found';
|
||||||
|
filteredMessage.value =
|
||||||
|
'없는 페이지 주소입니다. 잠시 후 다시 시도해 주세요.';
|
||||||
|
} else if (props.error.toString().startsWith('FetchError: ')) {
|
||||||
|
filteredCode.value = 503;
|
||||||
|
filteredTitle.value = 'Network Error';
|
||||||
|
filteredMessage.value =
|
||||||
|
'서버와 통신할 수 없습니다. 인터넷 연결을 확인하시고 잠시 후 다시 시도해 주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredTitle.value == '' && props.error.errorCode == undefined) {
|
||||||
|
console.log('unhandled error=', props.error);
|
||||||
|
|
||||||
|
filteredCode.value = 0;
|
||||||
|
filteredTitle.value = 'Unhandled Error';
|
||||||
|
filteredMessage.value = props.error.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = (nextPath) => clearError({ redirect: nextPath });
|
||||||
|
</script>
|
||||||
39
inspond-nuxt-safekiso/nuxt.config.ts
Normal file
39
inspond-nuxt-safekiso/nuxt.config.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { defineNuxtConfig } from 'nuxt';
|
||||||
|
|
||||||
|
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
ssr: false,
|
||||||
|
extends: ['./base'],
|
||||||
|
autoImports: {
|
||||||
|
dirs: ['config'],
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
name: 'viewport',
|
||||||
|
content: 'width=device-width, initial-scale=1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
link: [{ rel: 'stylesheet', href: 'https://rsms.me/inter/inter.css' }],
|
||||||
|
|
||||||
|
script: [],
|
||||||
|
},
|
||||||
|
css: ['~/base/assets/css/tailwind.pcss'],
|
||||||
|
build: {
|
||||||
|
postcss: {
|
||||||
|
postcssOptions: {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transpile: [
|
||||||
|
'@fawmi/vue-google-maps',
|
||||||
|
'@headlessui/vue',
|
||||||
|
'chart.js',
|
||||||
|
'@vuepic/vue-datepicker',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
modules: ['@nuxt/content'],
|
||||||
|
});
|
||||||
22527
inspond-nuxt-safekiso/package-lock.json
generated
Normal file
22527
inspond-nuxt-safekiso/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
inspond-nuxt-safekiso/package.json
Normal file
48
inspond-nuxt-safekiso/package.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "cross-env NITRO_PRESET=node nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"lint": "eslint --ext .ts,.vue",
|
||||||
|
"lint:fix": "eslint --ext .ts,.vue --fix",
|
||||||
|
"deploy:dev": "tar -czf client.tgz .output/ && aws s3 mv client.tgz s3://bucket-g9bbf2/pond_v2:safekiso/dist/dev/ --profile safekiso",
|
||||||
|
"deploy:mng": "tar -czf client.tgz .output/ && aws s3 mv client.tgz s3://bucket-g9bbf2/pond_v2:safekiso/dist/mng/ --profile safekiso",
|
||||||
|
"deploy:prod": "tar -czf client.tgz .output/ && aws s3 mv client.tgz s3://bucket-g9bbf2/pond_v2:safekiso/dist/prod/ --profile safekiso"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@headlessui/vue": "^1.6.5",
|
||||||
|
"@heroicons/vue": "^2.0.10",
|
||||||
|
"@nuxt/content": "^2.0.1",
|
||||||
|
"@nuxtjs/moment": "^1.6.1",
|
||||||
|
"@nuxtjs/tailwindcss": "^5.3.1",
|
||||||
|
"@tailwindcss/aspect-ratio": "^0.4.0",
|
||||||
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||||
|
"@typescript-eslint/parser": "^5.29.0",
|
||||||
|
"@vue/eslint-config-typescript": "^11.0.0",
|
||||||
|
"autoprefixer": "^10.4.7",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^8.18.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-vue": "^9.1.1",
|
||||||
|
"nuxt": "3.0.0-rc.4",
|
||||||
|
"postcss": "^8.4.14",
|
||||||
|
"tailwindcss": "^3.1.4",
|
||||||
|
"vue-eslint-parser": "^9.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fawmi/vue-google-maps": "^0.9.72",
|
||||||
|
"@vuepic/vue-datepicker": "^3.4.4",
|
||||||
|
"@vueup/vue-quill": "^1.0.0-beta.9",
|
||||||
|
"chart.js": "^3.8.0",
|
||||||
|
"dayjs": "^1.11.3",
|
||||||
|
"quill-blot-formatter": "^1.0.5",
|
||||||
|
"vue-chartjs": "^4.1.1",
|
||||||
|
"vue-json-pretty": "^2.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user