Merge branch 'main' into login

This commit is contained in:
yoon 2025-02-04 13:06:30 +09:00
commit e038c84baf
4 changed files with 184 additions and 115 deletions

View File

@ -1,18 +1,25 @@
<template> <template>
<div> <div>
<!-- 한글 버튼들 --> <ul class="alphabet-list list-unstyled d-flex flex-wrap mb-0">
<ul> <li v-for="char in koreanChars" :key="char" class="mt-2 mx-1">
<li v-for="char in koreanChars" :key="char" > <button
<button type="button" class="nav-link" @click="filterByKoreanChar(char)"> type="button"
class="btn"
:class="selectedAlphabet === char ? 'btn-primary' : 'btn-outline-primary'"
@click="selectAlphabet(char)"
>
{{ char }} {{ char }}
</button> </button>
</li> </li>
</ul> </ul>
<ul class="alphabet-list list-unstyled d-flex flex-wrap mb-0">
<!-- 영어 버튼들 --> <li v-for="char in englishChars" :key="char" class="mt-2 mx-1">
<ul> <button
<li v-for="char in englishChars" :key="char"> type="button"
<button type="button" class="nav-link" @click="filterByEnglishChar(char)"> class="btn"
:class="selectedAlphabet === char ? 'btn-primary' : 'btn-outline-primary'"
@click="selectAlphabet(char)"
>
{{ char }} {{ char }}
</button> </button>
</li> </li>
@ -21,28 +28,29 @@
</template> </template>
<script setup> <script setup>
import { defineProps } from 'vue'; import { ref } from 'vue';
const koreanChars = ['ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'];
const englishChars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
const selectedAlphabet = ref(null);
//emit
const emit = defineEmits(); const emit = defineEmits();
const selectAlphabet = (alphabet) => {
const koreanChars = ['ㄱ', 'ㄴ', 'ㄷ', 'ㄹ']; selectedAlphabet.value = selectedAlphabet.value === alphabet ? null : alphabet;
const englishChars = ['a', 'b', 'c', 'd', 'e']; emit('update:data',selectedAlphabet.value);
//
const filterByKoreanChar = (char) => {
emit('filter', char, 'korean');
};
//
const filterByEnglishChar = (char) => {
emit('filter', char, 'english');
}; };
</script> </script>
<style scoped> <style scoped>
.nav-pills { .alphabet-list {
display: flex; margin-left: -0.25rem;
justify-content: space-between; }
margin-top: 10px;
@media (max-width: 768px) {
.alphabet-list {
overflow: scroll;
flex-wrap: nowrap !important;
}
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<li class="mt-5"> <li class="mt-5 card p-5">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="w-100 d-flex align-items-center"> <div class="w-100 d-flex align-items-center">
<span class="btn btn-primary">{{ item.category }}</span> <span class="btn btn-primary">{{ item.category }}</span>
@ -7,7 +7,7 @@
</div> </div>
<EditBtn /> <EditBtn />
</div> </div>
<p class="mt-5">{{ item.WRDDICCON }}</p> <p class="mt-5" v-html="$common.contentToHtml(item.WRDDICCON)"></p>
<div class="d-flex justify-content-between flex-wrap gap-2 mb-2"> <div class="d-flex justify-content-between flex-wrap gap-2 mb-2">
<div class="d-flex flex-wrap align-items-center mb-50"> <div class="d-flex flex-wrap align-items-center mb-50">
<div class="avatar avatar-sm me-2"> <div class="avatar avatar-sm me-2">

View File

@ -22,7 +22,7 @@
<label class="col-md-2 col-form-label">카테고리 <span class="text-danger">*</span></label> <label class="col-md-2 col-form-label">카테고리 <span class="text-danger">*</span></label>
<div class="d-flex flex-wrap align-items-center mt-3 ms-1"> <div class="d-flex flex-wrap align-items-center mt-3 ms-1">
<div <div
v-for="(categoryName, index) in categoryList" v-for="(category, index) in categoryList"
:key="index" :key="index"
class="form-check me-3" class="form-check me-3"
> >
@ -30,18 +30,19 @@
class="form-check-input" class="form-check-input"
type="radio" type="radio"
:id="`category-${index}`" :id="`category-${index}`"
:value="index" :value="category.CMNCODVAL"
v-model="category" v-model="categoryValue"
/> />
<label class="form-check-label" :for="`category-${index}`"> <label class="form-check-label" :for="`category-${index}`">
{{ categoryName }} {{ category.CMNCODNAM }}
</label> </label>
</div> </div>
</div> </div>
<div class="invalid-feedback" :class="categoryAlert ? 'display-block' : ''">카테고리를 선택해주세요.</div>
</div> </div>
<!-- 비밀번호 필드 --> <!-- 비밀번호 필드 -->
<div v-if="category === 1" class="mb-4"> <div v-if="categoryValue === 300102" class="mb-4">
<FormInput <FormInput
title="비밀번호" title="비밀번호"
name="pw" name="pw"
@ -94,25 +95,39 @@
import QEditor from '@c/editor/QEditor.vue'; import QEditor from '@c/editor/QEditor.vue';
import FormInput from '@c/input/FormInput.vue'; import FormInput from '@c/input/FormInput.vue';
import FormFile from '@c/input/FormFile.vue'; import FormFile from '@c/input/FormFile.vue';
import { getCurrentInstance, ref } from 'vue'; import { getCurrentInstance, ref, onMounted } from 'vue';
import router from '@/router'; import router from '@/router';
import axios from '@api'; import axios from '@api';
const categoryList = ['자유', '익명', '공지사항']; // const categoryList = ref([]);
const title = ref(''); const title = ref('');
const password = ref(''); const password = ref('');
const category = ref(0); // 0 const categoryValue = ref(null);
const content = ref(''); const content = ref('');
const attachFiles = ref(null); const attachFiles = ref(null);
const isFileValid = ref(true); // const isFileValid = ref(true);
const titleAlert = ref(false); const titleAlert = ref(false);
const passwordAlert = ref(false); const passwordAlert = ref(false);
const contentAlert = ref(false); const contentAlert = ref(false);
const categoryAlert = ref(false);
const attachFilesAlert = ref(false); const attachFilesAlert = ref(false);
const { appContext } = getCurrentInstance(); const { appContext } = getCurrentInstance();
const $common = appContext.config.globalProperties.$common; // $common const $common = appContext.config.globalProperties.$common;
const fetchCategories = async () => {
try {
const response = await axios.get('board/categories');
categoryList.value = response.data.data;
} catch (error) {
console.error('카테고리 불러오기 오류:', error);
}
};
onMounted(() => {
fetchCategories();
});
const goList = () => { const goList = () => {
router.push('/board'); router.push('/board');
@ -120,10 +135,11 @@ const goList = () => {
const write = async () => { const write = async () => {
titleAlert.value = !title.value; titleAlert.value = !title.value;
passwordAlert.value = category.value === 1 && !password.value; passwordAlert.value = categoryValue.value === 300102 && !password.value;
contentAlert.value = !content.value; contentAlert.value = !content.value;
categoryAlert.value = !categoryValue.value;
if (titleAlert.value || passwordAlert.value || contentAlert.value || !isFileValid.value) { if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
return; return;
} }
@ -131,8 +147,8 @@ const write = async () => {
const boardData = { const boardData = {
LOCBRDTTL: title.value, LOCBRDTTL: title.value,
LOCBRDCON: $common.deltaAsJson(content.value), LOCBRDCON: $common.deltaAsJson(content.value),
LOCBRDPWD: category.value === 1 ? password.value : null, LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
LOCBRDTYP: category.value === 1 ? 'S' : 'F', LOCBRDTYP: categoryValue.value
}; };
const { data: boardResponse } = await axios.post('board', boardData); const { data: boardResponse } = await axios.post('board', boardData);

View File

@ -1,26 +1,26 @@
<template> <template>
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y">
<div class="card"> <div class="card p-5">
<!-- 타이틀, 검색 --> <!-- 타이틀, 검색 -->
<div class="row mt-4"> <div class="row">
<div class="col-6"> <div class="col-12 col-md-6">
<h5 class="mb-0">용어집</h5> <h5 class="mb-0 title">용어집</h5>
</div> </div>
<div class="col-6"> <div class="col-12 col-md-6">
<SearchBar @update:data="search"/> <SearchBar @update:data="search"/>
</div> </div>
</div> </div>
<!-- 단어 갯수, 작성하기 --> <!-- 단어 갯수, 작성하기 -->
<div class="mt-4"> <div class="mt-4">
단어 : {{ filteredList.length }} 단어 : {{ total }}
<WriteButton @click="toggleWriteForm" /> <WriteButton @click="toggleWriteForm" />
</div> </div>
<!-- --> <!-- -->
<div> <div>
<DictAlphabetFilter/> <DictAlphabetFilter @update:data="handleSelectedAlphabetChange" />
</div> </div>
<!-- 카테고리 --> <!-- 카테고리 -->
@ -32,36 +32,43 @@
<div v-if="isWriteVisible" class="mt-5"> <div v-if="isWriteVisible" class="mt-5">
<DictWrite @close="isWriteVisible = false" /> <DictWrite @close="isWriteVisible = false" />
</div> </div>
<!-- 용어 리스트 -->
<div class="mt-10">
<!-- 로딩 중일 -->
<div v-if="loading">로딩 중...</div>
<!-- 에러 메시지 -->
<div v-if="error" class="error">{{ error }}</div>
<!-- 단어 목록 -->
<ul v-if="filteredList.length" class="px-0 list-unstyled">
<DictCard
v-for="item in filteredList"
:key="item.WRDDICSEQ"
:item="item"
/>
</ul>
<!-- 데이터가 없을 -->
<div v-else-if="!loading && !error">용어집의 용어가 없습니다.</div>
</div>
</div> </div>
<!-- 용어 리스트 -->
<div class="mt-10">
<!-- 로딩 중일 -->
<div v-if="loading">로딩 중...</div>
<!-- 에러 메시지 -->
<div v-if="error" class="error">{{ error }}</div>
<!-- 단어 목록 -->
<ul v-if="total > 0" class="px-0 list-unstyled">
<DictCard
v-for="item in wordList"
:key="item.WRDDICSEQ"
:item="item"
/>
</ul>
<!-- <ul v-if="wordList.length > 0" class="px-0 list-unstyled">
<DictCard
v-for="item in wordList"
:key="item.WRDDICSEQ"
:item="item"
/>
</ul> -->
<!-- 데이터가 없을 -->
<div v-else-if="!loading && !error">용어집의 용어가 없습니다.</div>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, watchEffect, computed } from 'vue'; import { ref, watchEffect, computed, onMounted } from 'vue';
import axios from '@api'; import axios from '@api';
import SearchBar from '@c/search/SearchBar.vue'; import SearchBar from '@c/search/SearchBar.vue';
import WriteButton from '@c/button/WriteBtn.vue'; import WriteButton from '@c/button/WriteBtn.vue';
@ -77,72 +84,110 @@
// //
const wordList = ref([]); const wordList = ref([]);
//
const total = ref(0);
// //
const cateList = ref([]); const cateList = ref([]);
//
const selectedAlphabet = ref('');
// //
const searchText = ref(''); const searchText = ref('');
// //
const isWriteVisible = ref(false); const isWriteVisible = ref(false);
// API //
const fetchAllData = async () => { onMounted(() => {
loading.value = true; getwordList(); //
error.value = ''; getwordCategory(); //
try { });
// //
const wordResponse = await axios.get('worddict/getWordList'); const getwordList = (searchKeyword='',indexKeyword='') => {
wordList.value = wordResponse.data.data.data; axios.get('worddict/getWordList',{
console.log('용어집 데이터:', wordList.value); //
// params: { searchKeyword: searchKeyword
const categoryResponse = await axios.get('worddict/getWordCategory'); ,indexKeyword:indexKeyword
cateList.value = categoryResponse.data.data; }
console.log('카테고리 데이터:', cateList.value); })
} catch (err) { .then(res => {
error.value = '데이터를 가져오는 중 문제가 발생했습니다.'; wordList.value = res.data.data.data; //
} finally { total.value = res.data.data.total; //
loading.value = false; loading.value = false;
} })
.catch(err => {
console.error('데이터 로드 오류:', err);
error.value = '데이터를 가져오는 중 문제가 발생했습니다.';
loading.value = false; //
});
};
//
const getwordCategory = () => {
axios.get('worddict/getWordCategory')
.then(res => {
cateList.value = res.data.data; //
})
.catch(err => {
console.error('카테고리 로드 오류:', err);
error.value = '카테고리 데이터를 가져오는 중 문제가 발생했습니다.';
});
};
const handleSelectedAlphabetChange = (newAlphabet) => {
selectedAlphabet.value = newAlphabet;
getwordList(searchText.value,selectedAlphabet.value);
}; };
// //
const search = (e) => { const search = (e) => {
const trimmedSearchText = e.trim(); searchText.value = e.trim();
if (trimmedSearchText.length === 0) { getwordList(searchText.value,selectedAlphabet.value);
alert('검색어를 입력해주세요.');
return;
}
searchText.value = trimmedSearchText;
}; };
// API
// const fetchAllData = async () => {
// loading.value = true;
// error.value = '';
// try {
// //
// // const wordResponse = await axios.get('worddict/getWordList');
// //wordList.value = wordResponse.data.data.data;
// //console.log(' :', wordList.value);
// //
// const categoryResponse = await axios.get('worddict/getWordCategory');
// cateList.value = categoryResponse.data.data;
// console.log(' :', cateList.value);
// } catch (err) {
// error.value = ' .';
// } finally {
// loading.value = false;
// }
// };
// //
const filteredList = computed(() => // const filteredList = computed(() =>
wordList.value.filter(item => // wordList.value.filter(item =>
item.WRDDICTTL.toLowerCase().includes(searchText.value.toLowerCase()) // item.WRDDICTTL.toLowerCase().includes(searchText.value.toLowerCase())
) // )
); // );
// toggle // toggle
const toggleWriteForm = () => { const toggleWriteForm = () => {
isWriteVisible.value = !isWriteVisible.value; isWriteVisible.value = !isWriteVisible.value;
}; };
// `watchEffect` API
watchEffect(() => {
fetchAllData();
});
</script> </script>
<style scoped> <style scoped>
.card > div {
padding: 0 30px
}
.error { .error {
color: red; color: red;
font-weight: bold; font-weight: bold;
} }
@media (max-width: 768px) {
.title {
margin-bottom: 0.5rem !important;
}
}
</style> </style>