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

View File

@@ -0,0 +1,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>

View 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>