Merge remote-tracking branch 'origin/main' into board-view-content

This commit is contained in:
kimdaae328 2025-02-07 14:16:40 +09:00
commit f631420fdf
16 changed files with 2032 additions and 1092 deletions

2137
package-lock.json generated

File diff suppressed because it is too large Load Diff

38
src/common/commonApi.js Normal file
View File

@ -0,0 +1,38 @@
/*
작성자 : 박지윤
작성일 : 2025-02-04
수정자 :
수정일 :
설명 : 공통 api
*/
import { ref, onMounted } from "vue";
import $api from '@api';
const commonApi = () => {
const colorList = ref([]);
const mbtiList = ref([]);
const pwhintList = ref([]);
const CommonCode = async (endpoint, targetList) => {
try {
const response = await $api.get(`/user/${endpoint}`);
targetList.value = response.data.data.map(item => ({
label: item.CMNCODNAM,
value: item.CMNCODVAL
}));
} catch (error) {
console.error(`Error fetching ${endpoint}:`, error);
targetList.value = [];
}
};
onMounted(async () => {
await CommonCode("color", colorList);
await CommonCode("mbti", mbtiList);
await CommonCode("pwhint", pwhintList);
});
return { colorList, mbtiList, pwhintList };
};
export default commonApi;

View File

@ -43,4 +43,15 @@ const selectCategory = (cate) => {
.cate-list {
margin-left: -0.25rem;
}
@media (max-width: 768px) {
.cate-list {
overflow-x: scroll;
flex-wrap: nowrap !important;
li {
flex: 0 0 auto;
}
}
}
</style>

View File

@ -16,7 +16,7 @@
</template>
<script setup>
import { ref, watch, watchEffect } from 'vue';
import { ref, watch } from 'vue';
const props = defineProps({
title: {
@ -69,13 +69,16 @@ const props = defineProps({
const emit = defineEmits(['update:data']);
const selectData = ref(props.value);
watchEffect(() => {
if (props.isCommon && props.data.length > 0) {
selectData.value = props.data[0].value; //
} else {
selectData.value = props.value; //
// data
watch(() => props.data, (newData) => {
if (props.isCommon && newData.length > 0) {
selectData.value = newData[0].value;
emit('update:data', selectData.value);
}
emit('update:data', selectData.value);
})
}, { immediate: true });
// selectData
watch(selectData, (newValue) => {
emit('update:data', newValue);
});
</script>

View File

@ -23,6 +23,7 @@
:id="name"
class="form-control"
:type="type"
:max="type === 'date' ? today : null"
@input="updateInput"
:value="computedValue"
:maxLength="maxlength"

View File

@ -0,0 +1,173 @@
<template>
<div class="col-xl-12">
<UserFormInput
title="아이디"
name="id"
:is-alert="idAlert"
:is-essential="true"
:useInputGroup="true"
@update:data="handleIdChange"
:value="id"
/>
<UserFormInput
title="생년월일"
name="birth"
:type="'date'"
:is-essential="true"
:is-alert="birthAlert"
@update:data="birth = $event"
@update:alert="birthAlert = $event"
:value="birth"
/>
<FormSelect
title="비밀번호 힌트"
name="pwhint"
:is-essential="true"
:is-row="false"
:is-label="true"
:is-common="true"
:data="pwhintList"
@update:data="pwhint = $event"
/>
<UserFormInput
title="답변"
name="pwhintRes"
:is-essential="true"
:is-alert="pwhintResAlert"
@update:data="pwhintRes = $event"
@update:alert="pwhintResAlert = $event"
:value="pwhintRes"
/>
<div class="d-flex mt-5">
<RouterLink type="button" class="btn btn-secondary me-2 w-50" to="/login">취소</RouterLink>
<button type="button" @click="handleSubmit" class="btn btn-primary w-50">확인</button>
</div>
</div>
<div v-if="resetForm" class="mt-4">
<UserFormInput
title="새 비밀번호"
name="pw"
type="password"
:isEssential="true"
:is-alert="passwordAlert"
@update:data="password = $event"
@update:alert="passwordAlert = $event"
:value="password"
/>
<UserFormInput
title="비밀번호 확인"
name="pwch"
type="password"
:isEssential="true"
:is-alert="passwordcheckAlert"
@update:data="passwordcheck = $event"
@update:alert="passwordcheckAlert = $event"
@blur="checkPw"
:value="passwordcheck"
/>
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
<div class="d-grid gap-2 mt-5 mb-5">
<button type="button" @click="handleNewPassword" class="btn btn-primary">확인</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import $api from '@api';
import commonApi from '@/common/commonApi';
import { useRouter } from 'vue-router';
import { useToastStore } from '@s/toastStore';
import UserFormInput from '@c/input/UserFormInput.vue';
import FormSelect from '../input/FormSelect.vue';
const router = useRouter();
const toastStore = useToastStore();
const id = ref('');
const birth = ref('');
const pwhint = ref('');
const pwhintRes = ref('');
const idAlert = ref(false);
const birthAlert = ref(false);
const pwhintResAlert = ref(false);
const resetForm = ref(false);
const password = ref('');
const passwordcheck = ref('');
const passwordcheckError = ref('');
const passwordAlert = ref(false);
const passwordcheckAlert = ref(false);
const passwordcheckErrorAlert = ref(false);
const { pwhintList } = commonApi();
const handleIdChange = value => {
id.value = value;
idAlert.value = false;
};
// , , , member input
const handleSubmit = async () => {
idAlert.value = id.value.trim() === '';
pwhintResAlert.value = pwhintRes.value.trim() === '';
birthAlert.value = birth.value.trim() === '';
if (idAlert.value || pwhintResAlert.value || birthAlert.value) {
return;
}
const response = await $api.post('user/pwReset', {
id: id.value,
birth: birth.value,
pwhint: pwhint.value,
pwhintRes: pwhintRes.value,
});
if (response.status === 200 && response.data.data === true) {
resetForm.value = true;
} else {
toastStore.onToast('일치하는 정보가 없습니다.', 'e');
}
};
const checkPw = async () => {
if (password.value !== passwordcheck.value) {
passwordcheckError.value = '비밀번호가 일치하지 않습니다.';
passwordcheckErrorAlert.value = true;
} else {
passwordcheckError.value = '';
passwordcheckErrorAlert.value = false;
}
};
//
const handleNewPassword = async () => {
passwordAlert.value = password.value.trim() === '';
passwordcheckAlert.value = passwordcheck.value.trim() === '';
if (passwordAlert.value || passwordcheckAlert.value || passwordcheckErrorAlert.value) {
return;
}
const response = await $api.patch('user/pwNew', {
id: id.value,
password: password.value
});
if (response.status === 200 && response.data.data === true) {
toastStore.onToast('비밀번호가 재설정 되었습니다.', 's');
router.push('/login');
}
};
</script>

View File

@ -25,7 +25,7 @@
<div class="mb-3 d-flex justify-content-around">
<div>
<input type="checkbox" class="form-check-input" id="rememberCheck" />
<input type="checkbox" class="form-check-input" id="rememberCheck" v-model="remember" />
<label class="form-check-label fw-bold" for="rememberCheck">&nbsp;자동로그인</label>
</div>
<RouterLink class="text-dark fw-bold" to="/register">등록신청</RouterLink>
@ -40,11 +40,17 @@
import router from '@/router';
import { ref } from 'vue';
import UserFormInput from '@c/input/UserFormInput.vue';
import { useUserStore } from '@s/useUserStore';
import { useToastStore } from '@s/toastStore';
const id = ref('');
const password = ref('');
const idAlert = ref(false);
const passwordAlert = ref(false);
const remember = ref(false);
const userStore = useUserStore();
const toastStore = useToastStore();
const handleIdChange = value => {
id.value = value;
@ -57,21 +63,19 @@
};
const handleSubmit = async () => {
const response = await $api.post('user/login', {
loginId: id.value,
password: password.value,
remember: remember.value,
});
try {
const response = await $api.post('user/login', {
loginId: id.value,
password: password.value,
remember: false,
});
if (response.status === 200) {
console.log('로그인 성공', response.data);
router.push('/');
}
} catch (error) {
console.error('로그인 실패', error);
if (response.status === 200) {
await userStore.userInfo();
router.push('/');
} else {
toastStore.onToast('아이디 혹은 비밀번호가 틀렸습니다.', 'e');
}
};

View File

@ -17,7 +17,7 @@
/>
</svg>
</span>
<h2>{{ title }}</h2>
<h3 class="fw-bold">{{ title }}</h3>
</div>
</template>

View File

@ -58,9 +58,11 @@
:is-essential="true"
:is-row="false"
:is-label="true"
:is-common="true"
:data="pwhintList"
@update:data="pwhint = $event"
/>
<UserFormInput
title="답변"
name="pwhintRes"
@ -152,8 +154,9 @@
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref } from 'vue';
import $api from '@api';
import commonApi from '@/common/commonApi'
import UserFormInput from '@c/input/UserFormInput.vue';
import FormSelect from '@c/input/FormSelect.vue';
import ArrInput from '@c/input/ArrInput.vue';
@ -161,8 +164,6 @@
import { useRouter } from 'vue-router';
import { useToastStore } from '@s/toastStore';
const pwhintList = ['현재 살고 있는 동네', '가장 기억에 남는 책', '좋아하는 음식'];
const router = useRouter();
const profile = ref(null);
@ -173,7 +174,6 @@
const password = ref('');
const passwordcheck = ref('');
const passwordcheckError = ref('');
const pwhint = ref(0);
const pwhintRes = ref('');
const name = ref('');
const birth = ref('');
@ -181,10 +181,9 @@
const detailAddress = ref('');
const postcode = ref(''); //
const phone = ref('');
const colorList = ref([]);
const mbtiList = ref([]);
const color = ref(''); // color
const mbti = ref(''); // MBTI
const pwhint = ref(''); // pwhint
const profilAlert = ref(false);
const idAlert = ref(false);
@ -252,27 +251,8 @@
}
};
const Colors = async () => {
const response = await $api.get('/user/color');
colorList.value = response.data.data.map(item => ({
label: item.CMNCODNAM,
value: item.CMNCODVAL
}));
};
const Mbtis = async () => {
const response = await $api.get('/user/mbti');
mbtiList.value = response.data.data.map(item => ({
label: item.CMNCODNAM,
value: item.CMNCODVAL
}));
};
onMounted(() => {
Colors();
Mbtis();
});
// , mbti,
const { colorList, mbtiList, pwhintList } = commonApi();
//
const handleAddressUpdate = (addressData) => {
@ -295,50 +275,50 @@
//
const handleSubmit = async () => {
idAlert.value = id.value.trim() === '';
passwordAlert.value = password.value.trim() === '';
passwordcheckAlert.value = passwordcheck.value.trim() === '';
pwhintResAlert.value = pwhintRes.value.trim() === '';
nameAlert.value = name.value.trim() === '';
birthAlert.value = birth.value.trim() === '';
addressAlert.value = address.value.trim() === '';
phoneAlert.value = phone.value.trim() === '';
idAlert.value = id.value.trim() === '';
passwordAlert.value = password.value.trim() === '';
passwordcheckAlert.value = passwordcheck.value.trim() === '';
pwhintResAlert.value = pwhintRes.value.trim() === '';
nameAlert.value = name.value.trim() === '';
birthAlert.value = birth.value.trim() === '';
addressAlert.value = address.value.trim() === '';
phoneAlert.value = phone.value.trim() === '';
//
if (!profile.value) {
profilerr.value = '프로필 이미지를 선택해주세요.';
profilAlert.value = true;
} else {
profilerr.value = '';
profilAlert.value = false;
}
//
if (!profile.value) {
profilerr.value = '프로필 이미지를 선택해주세요.';
profilAlert.value = true;
} else {
profilerr.value = '';
profilAlert.value = false;
}
if (profilAlert.value || idAlert.value || idErrorAlert.value || passwordAlert.value || passwordcheckAlert.value ||
passwordcheckErrorAlert.value || pwhintResAlert.value || nameAlert.value || birthAlert.value || addressAlert.value || phoneAlert.value) {
return;
}
if (profilAlert.value || idAlert.value || idErrorAlert.value || passwordAlert.value || passwordcheckAlert.value ||
passwordcheckErrorAlert.value || pwhintResAlert.value || nameAlert.value || birthAlert.value || addressAlert.value || phoneAlert.value) {
return;
}
const formData = new FormData();
formData.append('memberIds', id.value);
formData.append('memberPwd', password.value);
formData.append('memberPwh', pwhintList[pwhint.value]);
formData.append('memberPwr', pwhintRes.value);
formData.append('memberNam', name.value);
formData.append('memberArr', address.value);
formData.append('memberDtl', detailAddress.value);
formData.append('memberZip', postcode.value);
formData.append('memberBth', birth.value);
formData.append('memberTel', phone.value);
formData.append('memberCol', color.value);
formData.append('memberMbt', mbti.value);
formData.append('memberPrf', profile.value);
const formData = new FormData();
formData.append('memberIds', id.value);
formData.append('memberPwd', password.value);
formData.append('memberPwh', pwhint.value);
formData.append('memberPwr', pwhintRes.value);
formData.append('memberNam', name.value);
formData.append('memberArr', address.value);
formData.append('memberDtl', detailAddress.value);
formData.append('memberZip', postcode.value);
formData.append('memberBth', birth.value);
formData.append('memberTel', phone.value);
formData.append('memberCol', color.value);
formData.append('memberMbt', mbti.value);
formData.append('memberPrf', profile.value);
const response = await $api.post('/user/join', formData, { isFormData : true });
const response = await $api.post('/user/join', formData, { isFormData : true });
if (response.status === 200) {
toastStore.onToast('등록신청이 완료되었습니다. 관리자 승인 후 이용가능합니다.', 's');
router.push('/login');
}
if (response.status === 200) {
toastStore.onToast('등록신청이 완료되었습니다. 관리자 승인 후 이용가능합니다.', 's');
router.push('/login');
}
};
</script>

View File

@ -1,18 +1,38 @@
<template>
<div class="d-flex">
<FormSelect :isLabel="false" :data="formattedDataList" :isCommon="true" name="용어집 카테고리" title="용어집 카테고리"/>
<button @click="toggleInput">+</button>
<FormSelect
name="cate"
title="카테고리"
:data="formattedDataList"
:is-common="true"
@update:data="selectCategory = $event"
@change="onChange"
/>
<PlusBtn @click="toggleInput"/>
<div class="d-flex" v-if="showInput">
<FormInput :isLabel="false" title="용어집 입력" name="용어집 입력"/>
<button @click="saveInput">저장</button>
<FormInput :isLabel="false" title="카테고리" name="카테고리" @update:modelValue="addCategory = $event" :is-alert="addCategoryAlert"/>
<button class="btn btn-primary" @click="saveInput">
<i class="bx bx-check"></i>
</button>
</div>
</div>
<div>
<FormInput title="내용" name="용어집 내용 입력"/>
<FormInput
title="용어"
type="text"
name="word"
:is-essential="true"
:is-alert="wordTitleAlert"
@update:modelValue="wordTitle = $event"
/>
</div>
<div>
<QEditor/>
<button>저장</button>
<QEditor @update:data="content = $event" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" />
<button class="btn btn-primary" @click="saveWord">
<i class="bx bx-check"></i>
</button>
<button @click="$emit('close')">취소</button>
</div>
</template>
@ -23,8 +43,23 @@ import { defineProps, computed, ref, defineEmits } from 'vue';
import QEditor from '@/components/editor/QEditor.vue';
import FormInput from '@/components/input/FormInput.vue';
import FormSelect from '@/components/input/FormSelect.vue';
import PlusBtn from '../button/PlusBtn.vue';
const emit = defineEmits(['close']);
const emit = defineEmits(['close','addCategory','addWord']);
//
const wordTitle = ref('');
const addCategory = ref('');
const content = ref('');
const imageUrls = ref([]);
// Vaildation
const wordTitleAlert = ref(false);
const wordContentAlert = ref(false);
const addCategoryAlert = ref(false);
//
const selectCategory = ref('');
const props = defineProps({
dataList: {
@ -46,12 +81,46 @@ const showInput = ref(false);
//
const toggleInput = () => {
howInput.value = !showInput.value;
showInput.value = !showInput.value;
};
//
const saveInput = () => {
console.log('입력값 저장됨!');
if(addCategory.value == ''){
addCategoryAlert.value = true;
return;
}
// console.log(' !',addCategory.value);
emit('addCategory', addCategory.value);
showInput.value = false;
};
</script>
const onChange = (newValue) => {
selectCategory.value = newValue.target.value;
};
//
const saveWord = () => {
//validation
//
if(wordTitle.value == ''){
wordTitleAlert.value = true;
return;
}
//
if(content.value == ''){
wordContentAlert.value = true;
return;
}
const wordData = {
title: wordTitle.value,
category: selectCategory.value,
content: content.value,
};
emit('addWord', wordData);
};
</script>

View File

@ -8,161 +8,14 @@
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
<ul class="navbar-nav flex-row align-items-center ms-auto">
<!-- Language -->
<li class="nav-item dropdown-language dropdown me-2 me-xl-0">
<a class="nav-link dropdown-toggle hide-arrow" href="javascript:void(0);" data-bs-toggle="dropdown">
<i class="bx bx-globe bx-md"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="javascript:void(0);" data-language="en" data-text-direction="ltr">
<span>English</span>
</a>
</li>
<li>
<a class="dropdown-item" href="javascript:void(0);" data-language="fr" data-text-direction="ltr">
<span>French</span>
</a>
</li>
<li>
<a class="dropdown-item" href="javascript:void(0);" data-language="ar" data-text-direction="rtl">
<span>Arabic</span>
</a>
</li>
<li>
<a class="dropdown-item" href="javascript:void(0);" data-language="de" data-text-direction="ltr">
<span>German</span>
</a>
</li>
</ul>
</li>
<!-- /Language -->
<!-- Style Switcher -->
<li class="nav-item dropdown-style-switcher dropdown me-2 me-xl-0">
<a class="nav-link dropdown-toggle hide-arrow" href="javascript:void(0);" data-bs-toggle="dropdown">
<i class="bx bx-md"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end dropdown-styles">
<li>
<a class="dropdown-item" href="javascript:void(0);" data-theme="light">
<span><i class="bx bx-sun bx-md me-3"></i>Light</span>
</a>
</li>
<li>
<a class="dropdown-item" href="javascript:void(0);" data-theme="dark">
<span><i class="bx bx-moon bx-md me-3"></i>Dark</span>
</a>
</li>
<li>
<a class="dropdown-item" href="javascript:void(0);" data-theme="system">
<span><i class="bx bx-desktop bx-md me-3"></i>System</span>
</a>
</li>
</ul>
</li>
<!-- / Style Switcher-->
<button class="btn p-1" @click="switchToLightMode"><i class='bx bxs-sun link-warning'></i></button>
<button class="btn p-1" @click="switchToDarkMode"><i class='bx bxs-moon' ></i></button>
<!-- Quick links -->
<li class="nav-item dropdown-shortcuts navbar-dropdown dropdown me-2 me-xl-0">
<a
class="nav-link dropdown-toggle hide-arrow"
href="javascript:void(0);"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
>
<i class="bx bx-grid-alt bx-md"></i>
</a>
<div class="dropdown-menu dropdown-menu-end p-0">
<div class="dropdown-menu-header border-bottom">
<div class="dropdown-header d-flex align-items-center py-3">
<h6 class="mb-0 me-auto">Shortcuts</h6>
<a
href="javascript:void(0)"
class="dropdown-shortcuts-add py-2"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Add shortcuts"
><i class="bx bx-plus-circle text-heading"></i
></a>
</div>
</div>
<div class="dropdown-shortcuts-list scrollable-container">
<div class="row row-bordered overflow-visible g-0">
<div class="dropdown-shortcuts-item col">
<span class="dropdown-shortcuts-icon rounded-circle mb-3">
<i class="bx bx-calendar bx-26px text-heading"></i>
</span>
<a href="app-calendar.html" class="stretched-link">Calendar</a>
<small>Appointments</small>
</div>
<div class="dropdown-shortcuts-item col">
<span class="dropdown-shortcuts-icon rounded-circle mb-3">
<i class="bx bx-food-menu bx-26px text-heading"></i>
</span>
<a href="app-invoice-list.html" class="stretched-link">Invoice App</a>
<small>Manage Accounts</small>
</div>
</div>
<div class="row row-bordered overflow-visible g-0">
<div class="dropdown-shortcuts-item col">
<span class="dropdown-shortcuts-icon rounded-circle mb-3">
<i class="bx bx-user bx-26px text-heading"></i>
</span>
<a href="app-user-list.html" class="stretched-link">User App</a>
<small>Manage Users</small>
</div>
<div class="dropdown-shortcuts-item col">
<span class="dropdown-shortcuts-icon rounded-circle mb-3">
<i class="bx bx-check-shield bx-26px text-heading"></i>
</span>
<a href="app-access-roles.html" class="stretched-link">Role Management</a>
<small>Permission</small>
</div>
</div>
<div class="row row-bordered overflow-visible g-0">
<div class="dropdown-shortcuts-item col">
<span class="dropdown-shortcuts-icon rounded-circle mb-3">
<i class="bx bx-pie-chart-alt-2 bx-26px text-heading"></i>
</span>
<a href="index.html" class="stretched-link">Dashboard</a>
<small>User Dashboard</small>
</div>
<div class="dropdown-shortcuts-item col">
<span class="dropdown-shortcuts-icon rounded-circle mb-3">
<i class="bx bx-cog bx-26px text-heading"></i>
</span>
<a href="pages-account-settings-account.html" class="stretched-link">Setting</a>
<small>Account Settings</small>
</div>
</div>
<div class="row row-bordered overflow-visible g-0">
<div class="dropdown-shortcuts-item col">
<span class="dropdown-shortcuts-icon rounded-circle mb-3">
<i class="bx bx-help-circle bx-26px text-heading"></i>
</span>
<a href="pages-faq.html" class="stretched-link">FAQs</a>
<small>FAQs & Articles</small>
</div>
<div class="dropdown-shortcuts-item col">
<span class="dropdown-shortcuts-icon rounded-circle mb-3">
<i class="bx bx-window-open bx-26px text-heading"></i>
</span>
<a href="modal-examples.html" class="stretched-link">Modals</a>
<small>Useful Popups</small>
</div>
</div>
</div>
</div>
</li>
<!-- Quick links -->
<button @click="switchToDarkMode">다크 모드</button>
<button @click="switchToLightMode">라이트 모드</button>
<i class="bx bx-bell bx-md bx-log-out cursor-pointer p-1" @click="handleLogout"></i>
<!-- Notification -->
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2">
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2 p-1">
<a
class="nav-link dropdown-toggle hide-arrow"
href="javascript:void(0);"
@ -284,120 +137,6 @@
</div>
</div>
</li>
<li class="list-group-item list-group-item-action dropdown-notifications-item marked-as-read">
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<div class="avatar">
<img src="/img/avatars/9.png" class="rounded-circle" />
</div>
</div>
<div class="flex-grow-1">
<h6 class="small mb-0">Application has been approved 🚀</h6>
<small class="mb-1 d-block text-body">Your ABC project application has been approved.</small>
<small class="text-muted">2 days ago</small>
</div>
<div class="flex-shrink-0 dropdown-notifications-actions">
<a href="javascript:void(0)" class="dropdown-notifications-read"
><span class="badge badge-dot"></span
></a>
<a href="javascript:void(0)" class="dropdown-notifications-archive"
><span class="bx bx-x"></span
></a>
</div>
</div>
</li>
<li class="list-group-item list-group-item-action dropdown-notifications-item marked-as-read">
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<div class="avatar">
<span class="avatar-initial rounded-circle bg-label-success"
><i class="bx bx-pie-chart-alt"></i
></span>
</div>
</div>
<div class="flex-grow-1">
<h6 class="small mb-0">Monthly report is generated</h6>
<small class="mb-1 d-block text-body">July monthly financial report is generated </small>
<small class="text-muted">3 days ago</small>
</div>
<div class="flex-shrink-0 dropdown-notifications-actions">
<a href="javascript:void(0)" class="dropdown-notifications-read"
><span class="badge badge-dot"></span
></a>
<a href="javascript:void(0)" class="dropdown-notifications-archive"
><span class="bx bx-x"></span
></a>
</div>
</div>
</li>
<li class="list-group-item list-group-item-action dropdown-notifications-item marked-as-read">
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<div class="avatar">
<img src="/img/avatars/5.png" class="rounded-circle" />
</div>
</div>
<div class="flex-grow-1">
<h6 class="small mb-0">Send connection request</h6>
<small class="mb-1 d-block text-body">Peter sent you connection request</small>
<small class="text-muted">4 days ago</small>
</div>
<div class="flex-shrink-0 dropdown-notifications-actions">
<a href="javascript:void(0)" class="dropdown-notifications-read"
><span class="badge badge-dot"></span
></a>
<a href="javascript:void(0)" class="dropdown-notifications-archive"
><span class="bx bx-x"></span
></a>
</div>
</div>
</li>
<li class="list-group-item list-group-item-action dropdown-notifications-item">
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<div class="avatar">
<img src="/img/avatars/6.png" class="rounded-circle" />
</div>
</div>
<div class="flex-grow-1">
<h6 class="small mb-0">New message from Jane</h6>
<small class="mb-1 d-block text-body">Your have new message from Jane</small>
<small class="text-muted">5 days ago</small>
</div>
<div class="flex-shrink-0 dropdown-notifications-actions">
<a href="javascript:void(0)" class="dropdown-notifications-read"
><span class="badge badge-dot"></span
></a>
<a href="javascript:void(0)" class="dropdown-notifications-archive"
><span class="bx bx-x"></span
></a>
</div>
</div>
</li>
<li class="list-group-item list-group-item-action dropdown-notifications-item marked-as-read">
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<div class="avatar">
<span class="avatar-initial rounded-circle bg-label-warning"
><i class="bx bx-error"></i
></span>
</div>
</div>
<div class="flex-grow-1">
<h6 class="small mb-0">CPU is running high</h6>
<small class="mb-1 d-block text-body">CPU Utilization Percent is currently at 88.63%,</small>
<small class="text-muted">5 days ago</small>
</div>
<div class="flex-shrink-0 dropdown-notifications-actions">
<a href="javascript:void(0)" class="dropdown-notifications-read"
><span class="badge badge-dot"></span
></a>
<a href="javascript:void(0)" class="dropdown-notifications-archive"
><span class="bx bx-x"></span
></a>
</div>
</div>
</li>
</ul>
</li>
<li class="border-top">
@ -413,9 +152,7 @@
<!-- User -->
<li class="nav-item navbar-dropdown dropdown-user dropdown">
<a class="nav-link dropdown-toggle hide-arrow p-0" href="javascript:void(0);" data-bs-toggle="dropdown">
<div class="avatar avatar-online">
<img src="/img/avatars/1.png" class="w-px-40 h-auto rounded-circle" />
</div>
<img v-if="user" :src="`http://localhost:10325/upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-40 h-auto rounded-circle"/>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
@ -490,17 +227,38 @@
</nav>
</template>
<script setup>
import { useAuthStore } from '@s/useAuthStore';
import { useUserStore } from '@s/useUserStore';
import { useRouter } from 'vue-router';
import { useThemeStore } from '@s/darkmode';
import { onMounted } from 'vue';
import { onMounted, ref } from 'vue';
const user = ref(null);
const authStore = useAuthStore();
const userStore = useUserStore();
const router = useRouter();
const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
onMounted(() => {
onMounted(async () => {
if (isDarkMode) {
switchToDarkMode();
} else {
switchToLightMode();
}
await userStore.userInfo();
user.value = userStore.user;
});
const handleLogout = async () => {
await authStore.logout();
router.push('/login');
};
</script>
<style></style>

View File

@ -1,9 +1,12 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@s/useAuthStore';
// 초기 렌더링 속도를 위해 지연 로딩 사용
const routes = [
{
path: '/',
name: "Home",
component: () => import('@v/MainView.vue'),
// meta: { requiresAuth: true }
},
@ -38,18 +41,26 @@ const routes = [
},
{
path: '/login',
name: 'Login',
component: () => import('@v/user/TheLogin.vue'),
meta: { layout: 'NoLayout' },
meta: { layout: 'NoLayout', requiresGuest: true },
},
{
path: '/register',
name: 'Register',
component: () => import('@v/user/TheRegister.vue'),
meta: { layout: 'NoLayout', requiresGuest: true },
},
{
path: '/pw',
name: 'Password',
component: () => import('@v/user/ThePassword.vue'),
meta: { layout: 'NoLayout', requiresGuest: true },
},
{
path: '/vacation',
component: () => import('@v/vacation/VacationManagement.vue'),
},
{
path: '/register',
component: () => import('@v/user/TheRegister.vue'),
meta: { layout: 'NoLayout' },
},
{
path: '/voteboard',
component: () => import('@v/voteboard/TheVoteBoard.vue'),
@ -80,4 +91,41 @@ const router = createRouter({
routes: routes,
})
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore();
await authStore.checkAuthStatus(); // 로그인 상태 확인
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
// 로그인이 필요한 페이지인데 로그인되지 않은 경우 → 로그인 페이지로 이동
next({ name: 'Login' });
} else if (to.meta.requiresGuest && authStore.isAuthenticated) {
// 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동
next({ name: 'Home' });
} else {
next();
}
});
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
// 최초 앱 로드 시 인증 상태 체크
await authStore.checkAuthStatus()
// 현재 라우트에 인증이 필요한지 확인
const requiresAuth = to.meta.requiresAuth === true
if (requiresAuth && !authStore.isAuthenticated) {
// 인증되지 않은 사용자를 로그인 페이지로 리다이렉트
// 원래 가려던 페이지를 쿼리 파라미터로 전달
next({
name: 'Login',
query: { redirect: to.fullPath }
})
} else {
next()
}
})
export default router

View File

@ -0,0 +1,50 @@
/*
작성자 : 박지윤
작성일 : 2025-02-04
수정자 :
수정일 :
설명 : 로그인 상태, 로그아웃
*/
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
import $api from "@api";
export const useAuthStore = defineStore('auth', () => {
const user = ref(null);
// 로그인 여부 확인
const isAuthenticated = computed(() => user.value !== null);
// 로그인 상태 확인
const checkAuthStatus = async () => {
const response = await $api.get('user/isLogin');
if (response.data.status === "OK" && response.data.data) {
user.value = response.data.data;
} else {
user.value = null;
}
};
// 로그아웃
const logout = async () => {
const response = await $api.get('user/logout');
// 로그아웃 성공 시 사용자 상태 초기화
if (response.data.status === "OK") {
user.value = null;
return true;
} else {
console.error("로그아웃 실패:", response.data.data);
return false;
}
};
return {
user,
isAuthenticated,
checkAuthStatus,
logout
};
});

View File

@ -0,0 +1,28 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import $api from "@api";
export const useUserStore = defineStore('userInfo', () => {
const user = ref(null);
// 사용자 정보 가져오기
const userInfo = async () => {
try {
const response = await $api.get('user/userInfo');
if (response.data.status === "OK") {
user.value = response.data.data;
console.log(user.value);
return true;
}
return false;
} catch (error) {
console.error("사용자 정보 조회 실패:", error);
return false;
}
};
return {
user,
userInfo,
};
});

View File

@ -0,0 +1,16 @@
<template>
<div class="d-flex justify-content-center align-items-center">
<div class="container rounded bg-white my-10 py-10" style="max-width: 500px">
<LogoHeader title="비밀번호 재설정"/>
<FindPassword />
</div>
</div>
</template>
<script setup>
import FindPassword from '@/components/user/FindPassword.vue';
import LogoHeader from '@c/user/LogoHeader.vue';
</script>
<style></style>

View File

@ -30,7 +30,7 @@
<!-- 작성 -->
<div v-if="isWriteVisible" class="mt-5">
<DictWrite @close="isWriteVisible = false" :dataList="cateList"/>
<DictWrite @close="isWriteVisible = false" :dataList="cateList" @addCategory="addCategory" @addWord="addWord"/>
</div>
</div>
@ -61,7 +61,7 @@
</template>
<script setup>
import { ref, watchEffect, computed, onMounted } from 'vue';
import { ref, watchEffect, computed, onMounted, getCurrentInstance } from 'vue';
import axios from '@api';
import SearchBar from '@c/search/SearchBar.vue';
import WriteButton from '@c/button/WriteBtn.vue';
@ -69,7 +69,12 @@
import DictCard from '@/components/wordDict/DictCard.vue';
import DictWrite from '@/components/wordDict/DictWrite.vue';
import DictAlphabetFilter from '@/components/wordDict/DictAlphabetFilter.vue';
import { useToastStore } from '@s/toastStore';
const { appContext } = getCurrentInstance();
const $common = appContext.config.globalProperties.$common;
const toastStore = useToastStore();
//
const loading = ref(false);
@ -87,7 +92,7 @@
//
const selectedAlphabet = ref('');
//
const searchText = ref('');
@ -99,12 +104,12 @@
getwordList();
getwordCategory();
});
//
const getwordList = (searchKeyword='', indexKeyword='', category='') => {
axios.get('worddict/getWordList',{
//
params: {
params: {
searchKeyword : searchKeyword,
indexKeyword :indexKeyword,
category : category
@ -157,6 +162,43 @@
isWriteVisible.value = !isWriteVisible.value;
};
//
const addCategory = (data) =>{
const lastCategory = cateList.value[cateList.value.length - 1];
const newValue = lastCategory ? parseInt(lastCategory.CMNCODVAL) + 1 : 600101;
axios.post('worddict/insertCategory',{
CMNCODNAM: data
}).then(res => {
if(res.data.data == '1'){
toastStore.onToast('카테고리가 추가 등록 되었습니다.', 's');
const newCategory = { CMNCODNAM: data, CMNCODVAL: newValue.toString() }; // Assuming CMNCODVAL is the same as CMNCODNAM
cateList.value.unshift(newCategory);
selectCategory.value = newCategory.CMNCODVAL;
}
})
}
//
const addWord = (wordData) => {
// console.log(' :', wordData.category); // WRDDICCAT
// console.log(' :', wordData.content); // WRDDICCON
// console.log(' :', wordData.title); // WRDDICTTL
axios.post('worddict/insertWord',{
WRDDICCAT : wordData.category,
WRDDICTTL : wordData.title,
WRDDICCON : $common.deltaAsJson(wordData.content),
}).then(res => {
if(res.data.data == '1'){
toastStore.onToast('용어가 등록 되었습니다.', 's');
isWriteVisible.value = false
// const newCategory = { CMNCODNAM: data, CMNCODVAL: newValue.toString() };
// cateList.value.unshift(newCategory);
// selectCategory.value = newCategory.CMNCODVAL;
}
})
};
</script>
@ -172,4 +214,4 @@
margin-bottom: 0.5rem !important;
}
}
</style>
</style>