Compare commits
No commits in common. "main" and "250407_park_boardlistbtn" have entirely different histories.
main
...
250407_par
@ -172,9 +172,6 @@
|
||||
.fc-toolbar-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
.fc-today-button {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
/* 클릭 가능한 날짜 */
|
||||
.fc-daygrid-day.clickable {
|
||||
cursor: pointer;
|
||||
@ -722,12 +719,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hidden-time-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 권한부여 */
|
||||
.user-card-container {
|
||||
display: flex;
|
||||
@ -752,7 +743,7 @@
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.switch {
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
comment: {
|
||||
@ -64,11 +64,6 @@
|
||||
const likeCount = computed(() => props.comment?.likeCount ?? props.likeCount);
|
||||
const dislikeCount = computed(() => props.comment?.dislikeCount ?? props.dislikeCount);
|
||||
|
||||
watch([() => props.likeClicked, () => props.dislikeClicked], ([likeNewval, dislikeNewval]) => {
|
||||
likeClicked.value = likeNewval;
|
||||
dislikeClicked.value = dislikeNewval;
|
||||
});
|
||||
|
||||
const handleLike = () => {
|
||||
const isLike = !likeClicked.value;
|
||||
const isDislike = false;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<div class="row g-0">
|
||||
<div class="col-3 border-end text-center" id="app-calendar-sidebar">
|
||||
<div class="card-body">
|
||||
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-50 h-px-50 rounded-circle object-fit-cover" @error="$event.target.src = '/img/icons/icon.png'"/>
|
||||
<img v-if="user" :src="`${baseUrl}upload/img/profile/${user.profile}`" alt="Profile Image" class="w-px-50 h-px-50 rounded-circle object-fit-contain" @error="$event.target.src = '/img/icons/icon.png'"/>
|
||||
<p class="mt-2 fw-bold">
|
||||
{{ user.name }}
|
||||
</p>
|
||||
@ -60,7 +60,7 @@
|
||||
<div class="row my-2 d-flex align-items-center">
|
||||
<div class="col-4">
|
||||
<img :src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
||||
class="me-2 w-px-50 h-px-50 rounded-circle object-fit-cover"
|
||||
class="me-2 w-px-50 h-px-50 rounded-circle object-fit-contain"
|
||||
@error="$event.target.src = '/img/icons/icon.png'">
|
||||
|
||||
<span class="fw-bold">{{ commuter.memberName }}</span>
|
||||
@ -399,7 +399,7 @@ const loadCommuters = async () => {
|
||||
// 프로필 이미지 생성
|
||||
const profileImg = document.createElement('img');
|
||||
profileImg.src = `${baseUrl}upload/img/profile/${commuter.profile}`;
|
||||
profileImg.className = 'rounded-circle w-px-20 h-px-20 mx-1 mb-1 position-relative z-5 m-auto object-fit-cover';
|
||||
profileImg.className = 'rounded-circle w-px-20 h-px-20 mx-1 mb-1 position-relative z-5 m-auto object-fit-contain';
|
||||
profileImg.style.border = `2px solid ${commuter.projctcolor}`;
|
||||
profileImg.onerror = () => { profileImg.src = '/img/icons/icon.png'; };
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
<img
|
||||
:src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
||||
alt="User Profile"
|
||||
class="rounded-circle object-fit-cover"
|
||||
class="rounded-circle object-fit-contain"
|
||||
:class="isCurrentUser(commuter) ? 'cursor-pointer' : ''"
|
||||
:draggable="isCurrentUser(commuter)"
|
||||
@dragstart="isCurrentUser(commuter) ? dragStart($event, post) : null"
|
||||
|
||||
@ -41,7 +41,6 @@
|
||||
|
||||
<button class="ql-link">Link</button>
|
||||
<button class="ql-image">Image</button>
|
||||
<button class="ql-video">Video</button>
|
||||
<button class="ql-blockquote">Blockquote</button>
|
||||
<button class="ql-code-block">Code Block</button>
|
||||
</div>
|
||||
@ -55,11 +54,8 @@
|
||||
<script setup>
|
||||
import Quill from 'quill';
|
||||
import 'quill/dist/quill.snow.css';
|
||||
import $api from '@api';
|
||||
import { onMounted, ref, watch, defineEmits, defineProps } from 'vue';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
|
||||
const toastStore = useToastStore();
|
||||
import $api from '@api';
|
||||
|
||||
const props = defineProps({
|
||||
isAlert: {
|
||||
@ -135,38 +131,6 @@
|
||||
initCheckImageIndex();
|
||||
}
|
||||
|
||||
// 영상 넣기
|
||||
quillInstance.getModule('toolbar').addHandler('video', () => {
|
||||
const url = prompt('YouTube 영상 URL을 입력하세요:');
|
||||
let src = '';
|
||||
if (!url || url.trim() == '') return;
|
||||
|
||||
// 일반 youtube url
|
||||
if (url.indexOf('watch?v=') !== -1) {
|
||||
src = url.replace('watch?v=', 'embed/');
|
||||
|
||||
// youtu.be 단축 URL (ex : https://youtu.be/CfiojceAaeQ?si=G7eM56sdDjIEw-Tz)
|
||||
} else if (url.indexOf('youtu.be/') !== -1) {
|
||||
const videoId = url.split('youtu.be/')[1].split('?')[0];
|
||||
src = `https://www.youtube.com/embed/${videoId}`;
|
||||
|
||||
// iframe 주소
|
||||
} else if (url.indexOf('<iframe') !== -1) {
|
||||
// DOMParser를 사용하여 embeded url만 추출
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(url, 'text/html');
|
||||
const iframeEL = doc.querySelector('iframe');
|
||||
src = iframeEL.getAttribute('src');
|
||||
} else {
|
||||
toastStore.onToast('지원하는 영상 타입 아님', 'e');
|
||||
return;
|
||||
}
|
||||
|
||||
const index = quillInstance.getSelection().index;
|
||||
quillInstance.insertEmbed(index, 'video', src);
|
||||
quillInstance.setSelection(index + 1);
|
||||
});
|
||||
|
||||
// 이미지 업로드 기능 처리
|
||||
let imageUrls = new Set(); // 업로드된 이미지 URL을 추적
|
||||
quillInstance.getModule('toolbar').addHandler('image', () => {
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
:value="computedValue"
|
||||
:disabled="disabled"
|
||||
:maxLength="maxlength"
|
||||
:placeholder="placeholder ? placeholder : title"
|
||||
:placeholder="title"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
<span class="input-group-text">@ localhost.co.kr</span>
|
||||
@ -29,7 +29,7 @@
|
||||
:value="computedValue"
|
||||
:disabled="disabled"
|
||||
:maxLength="maxlength"
|
||||
:placeholder="placeholder ? placeholder : title"
|
||||
:placeholder="title"
|
||||
@blur="$emit('blur')"
|
||||
@click="handleDateClick"
|
||||
ref="inputElement"
|
||||
@ -89,10 +89,6 @@
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:data', 'update:alert', 'blur']);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<div class="row g-0">
|
||||
<div class="card-body">
|
||||
<!-- 제목 -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex justify-content-between ">
|
||||
<h5 class="card-title fw-bold">
|
||||
{{ title }}
|
||||
</h5>
|
||||
@ -12,62 +12,42 @@
|
||||
<DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 날짜 -->
|
||||
<div class="row align-items-center pb-2">
|
||||
<div class="col-3 col-md-2 d-flex align-items-center">
|
||||
<i class="bx bx-calendar"></i>
|
||||
<div class="ms-2">날짜</div>
|
||||
</div>
|
||||
<div class="col-9 col-md-10">
|
||||
{{ strdate }} ~ {{ enddate }}
|
||||
</div>
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<i class="bx bx-calendar"></i>
|
||||
<div class="ms-2">날짜</div>
|
||||
<div class="ms-12">{{ strdate }} ~ {{ enddate }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 참여자 -->
|
||||
<div class="row align-items-center pb-2">
|
||||
<div class="col-3 col-md-2 d-flex align-items-center">
|
||||
<i class="bx bxs-user"></i>
|
||||
<div class="ms-2">참여자</div>
|
||||
</div>
|
||||
<div class="col-9 col-md-10">
|
||||
<UserList ref="userListRef" :projctSeq="projctSeq" :showOnlyActive="true" class="mb-0" />
|
||||
</div>
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<i class="bx bxs-user"></i>
|
||||
<div class="ms-2">참여자</div>
|
||||
<UserList ref="userListRef" :projctSeq="projctSeq" :showOnlyActive="true" class="ms-8 mb-0" />
|
||||
</div>
|
||||
|
||||
<!-- 설명 -->
|
||||
<div class="row align-items-center pb-2">
|
||||
<div class="col-3 col-md-2 d-flex align-items-center">
|
||||
<i class="bx bx-detail"></i>
|
||||
<div class="ms-2">설명</div>
|
||||
</div>
|
||||
<div class="col-9 col-md-10">
|
||||
{{ description || '-' }}
|
||||
</div>
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<i class="bx bx-detail"></i>
|
||||
<div class="ms-2">설명</div>
|
||||
<div class="ms-12">{{ description }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 주소 -->
|
||||
<div class="row align-items-center pb-2">
|
||||
<div class="col-3 col-md-2 d-flex align-items-center">
|
||||
<MapPopover
|
||||
:address="address"
|
||||
:is-visible="isMapVisible"
|
||||
@update-visible="updatePopover"
|
||||
>
|
||||
<template #trigger>
|
||||
<div class="d-flex align-items-center cursor-pointer">
|
||||
<i class="bx bxs-map"></i>
|
||||
<div class="ms-2">주소</div>
|
||||
</div>
|
||||
</template>
|
||||
</MapPopover>
|
||||
</div>
|
||||
<div class="col-9 col-md-10 d-flex justify-content-between align-items-center">
|
||||
<div>{{ address }} {{ addressdtail }}</div>
|
||||
<button type="button" class="btn text-white" :style="`background-color: ${projctColor} !important;`" @click.stop="openModal">
|
||||
<i class='bx bx-child'></i>
|
||||
</button>
|
||||
<div class="d-flex flex-sm-row align-items-center pb-2">
|
||||
<MapPopover
|
||||
:address="address"
|
||||
:is-visible="isMapVisible"
|
||||
@update-visible="updatePopover"
|
||||
>
|
||||
<template #trigger>
|
||||
<div class="d-flex align-items-center cursor-pointer">
|
||||
<i class="bx bxs-map"></i>
|
||||
<div class="ms-2">주소</div>
|
||||
</div>
|
||||
</template>
|
||||
</MapPopover>
|
||||
<div class="ms-12">
|
||||
{{ address }} {{ addressdtail }}
|
||||
</div>
|
||||
<button type="button" class="btn ms-auto text-white" :style="`background-color: ${projctColor} !important;`" @click.stop="openModal"><i class='bx bx-child'></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -541,19 +521,17 @@ const handleUpdate = async () => {
|
||||
|
||||
// 프로젝트 삭제
|
||||
const handleDelete = () => {
|
||||
if (confirm('프로젝트를 삭제하시겠습니까?')) {
|
||||
$api.patch('project/delete', {
|
||||
projctSeq: props.projctSeq,
|
||||
projctCol: props.projctCol,
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('프로젝트가 삭제되었습니다.', 's');
|
||||
projectStore.getProjectList();
|
||||
projectStore.getMemberProjects();
|
||||
}
|
||||
})
|
||||
}
|
||||
$api.patch('project/delete', {
|
||||
projctSeq: props.projctSeq,
|
||||
projctCol: props.projctCol,
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
toastStore.onToast('삭제가 완료되었습니다.', 's');
|
||||
projectStore.getProjectList();
|
||||
projectStore.getMemberProjects();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// 컴포넌트 마운트 시 실행
|
||||
|
||||
@ -33,21 +33,10 @@
|
||||
<span v-if="noInputAlert" class="invalid-feedback d-block" style="padding-left: 5px">{{ noInputAlert }}</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div @click="focusPicker">
|
||||
<input
|
||||
type="time"
|
||||
class="form-control form-control-sm py-1"
|
||||
style="height: 0%; font-size: 12px"
|
||||
v-model="eventTime"
|
||||
@input="handleChangeInput2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref="timeInput"
|
||||
type="time"
|
||||
class="hidden-time-input"
|
||||
style="height: 0%; font-size: 12px"
|
||||
class="form-control form-control-sm py-1"
|
||||
style="height: 25px; font-size: 12px"
|
||||
v-model="eventTime"
|
||||
@input="handleChangeInput2"
|
||||
/>
|
||||
@ -92,14 +81,6 @@
|
||||
const eventTime = ref('');
|
||||
const noInputAlert = ref(null);
|
||||
const noInputAlert2 = ref(null);
|
||||
const timeInput = ref(null);
|
||||
|
||||
const focusPicker = () => {
|
||||
if (timeInput.value) {
|
||||
timeInput.value.showPicker(); // 달력 자동 열기 (일부 브라우저에서 지원)
|
||||
timeInput.value.focus(); // 포커스 이동
|
||||
}
|
||||
};
|
||||
|
||||
const eventTypes = [
|
||||
{ type: 'birthdayParty', code: '300203', title: '생일파티' },
|
||||
|
||||
@ -14,14 +14,7 @@
|
||||
{{ user.name }}
|
||||
</p>
|
||||
|
||||
<CommuterBtn
|
||||
ref="workTimeComponentRef"
|
||||
:userId="user.id"
|
||||
:checkedInProject="checkedInProject || {}"
|
||||
:pendingProjectChange="pendingProjectChange"
|
||||
@update:pendingProjectChange="pendingProjectChange = $event"
|
||||
@leaveTimeUpdated="handleLeaveTimeUpdate"
|
||||
/>
|
||||
<CommuterBtn :userId="user.id" :checkedInProject="checkedInProject || {}" ref="workTimeComponentRef" />
|
||||
|
||||
<MainEventList
|
||||
:categoryList="categoryList"
|
||||
@ -48,7 +41,6 @@
|
||||
class="flatpickr-calendar-only"
|
||||
>
|
||||
</full-calendar>
|
||||
<input ref="calendarDatepicker" type="text" class="d-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -69,13 +61,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, onMounted, reactive, ref, watch, nextTick } from 'vue';
|
||||
import { inject, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { fetchHolidays } from '@c/calendar/holiday';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { useProjectStore } from '@/stores/useProjectStore';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import { useWeatherStore } from '@/stores/useWeatherStore';
|
||||
import { useDatePicker } from '@/stores/useDatePicker';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import router from '@/router';
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
@ -94,19 +85,16 @@
|
||||
const userStore = useUserInfoStore();
|
||||
const projectStore = useProjectStore();
|
||||
const weatherStore = useWeatherStore();
|
||||
const datePickerStore = useDatePicker();
|
||||
const { dailyWeatherList } = storeToRefs(weatherStore);
|
||||
|
||||
const dayjs = inject('dayjs');
|
||||
const fullCalendarRef = ref(null);
|
||||
const workTimeComponentRef = ref(null);
|
||||
const calendarEvents = ref([]);
|
||||
const calendarDatepicker = ref(null);
|
||||
//const dailyWeatherList = ref([]);
|
||||
|
||||
const selectedProject = ref(null);
|
||||
const checkedInProject = ref(null);
|
||||
const pendingProjectChange = ref(null);
|
||||
|
||||
// 이벤트 모달 관련
|
||||
const showModal = ref(false);
|
||||
@ -356,7 +344,7 @@
|
||||
useFilterEventList(month, day);
|
||||
};
|
||||
|
||||
// 오늘 날짜 노란색 배경 복구
|
||||
// 오늘 날짜 노란색 복구
|
||||
const colorToday = e => {
|
||||
if (todayEL != null && !todayEL.classList.contains('fc-day-today')) todayEL.classList.add('fc-day-today');
|
||||
};
|
||||
@ -518,13 +506,10 @@
|
||||
selectAllow: selectInfo => isSelectableDate(selectInfo.start),
|
||||
dateClick: handleDateClick,
|
||||
dayCellDidMount: arg => {
|
||||
// 날씨 정보 업데이트
|
||||
addWeatherInfo(arg);
|
||||
const dateCell = arg.el;
|
||||
|
||||
// 마우스 홀드시 이벤트 모달
|
||||
dateCell.addEventListener('mousedown', e => {
|
||||
if (!isSelectableDate(arg.date)) return; // 공휴일 제외
|
||||
const date = $common.dateFormatter(arg.date, 'YMD');
|
||||
handleMouseDown(date, e);
|
||||
});
|
||||
@ -551,51 +536,12 @@
|
||||
},
|
||||
});
|
||||
|
||||
// 날짜 정보 업데이트
|
||||
const addWeatherInfo = arg => {
|
||||
const dateStr = $common.dateFormatter(arg.date, 'YMD');
|
||||
// 해당 셀의 날짜와 일치하는 데이터
|
||||
const theDayWeatherInfo = dailyWeatherList.value.find(weather => weather.date === dateStr);
|
||||
const dayTopEl = arg.el.querySelector('.fc-daygrid-day-top');
|
||||
const isWeatherInfoExist = dayTopEl.getElementsByClassName('weather-icon').length > 0; // 중복 방지
|
||||
|
||||
if (theDayWeatherInfo && !isWeatherInfoExist) {
|
||||
let weatherIconUrl = `https://openweathermap.org/img/wn/${theDayWeatherInfo.icon}.png`;
|
||||
if (theDayWeatherInfo.icon === '01d' || theDayWeatherInfo.icon === '01n') {
|
||||
weatherIconUrl = '/img/icons/sunny-custom.png';
|
||||
}
|
||||
// 날씨 이미지 세팅
|
||||
const weatherEl = document.createElement('img');
|
||||
weatherEl.src = weatherIconUrl;
|
||||
weatherEl.alt = theDayWeatherInfo.description;
|
||||
weatherEl.className = 'weather-icon';
|
||||
weatherEl.style.width = '28px';
|
||||
weatherEl.style.height = '28px';
|
||||
|
||||
// 해당 셀에 이미지 넣기
|
||||
dayTopEl.classList.add('align-items-center');
|
||||
dayTopEl.prepend(weatherEl); // 이상하게 가장 앞에 넣어야 일자 뒤에 나옴 reverse 옵션이 있는듯
|
||||
}
|
||||
};
|
||||
|
||||
// 날씨 데이터 변경 감지하여 날씨 정보 업데이트
|
||||
watch(dailyWeatherList, async () => {
|
||||
await nextTick(); // DOM이 업데이트된 후 실행
|
||||
document.querySelectorAll('.fc-daygrid-day').forEach(dayCell => {
|
||||
addWeatherInfo({
|
||||
el: dayCell,
|
||||
date: dayCell.dataset.date,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const handleWheelEvent = e => {
|
||||
handleCloseModal();
|
||||
};
|
||||
|
||||
// 선택한 날의 이벤트 중에 휴가자 항목을 누를때 휴가페이지 이동
|
||||
const handleClickVacation = () => {
|
||||
router.push('/vacation');
|
||||
router.push({ path: 'Vacation' });
|
||||
};
|
||||
|
||||
// 달력 뷰 변경 감지 (월 변경 시 데이터 다시 가져오기)
|
||||
@ -606,55 +552,6 @@
|
||||
},
|
||||
);
|
||||
|
||||
// selectbox 프로젝트 선택 변경 사항 감지
|
||||
watch(
|
||||
() => projectStore.selectedProject,
|
||||
newProject => {
|
||||
if (newProject) {
|
||||
selectedProject.value = newProject.PROJCTSEQ;
|
||||
checkedInProject.value = newProject;
|
||||
} else {
|
||||
selectedProject.value = null;
|
||||
checkedInProject.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const handleLeaveTimeUpdate = async event => {
|
||||
const memberSeq = user.value.id;
|
||||
if (!memberSeq) return;
|
||||
|
||||
// 현 사용자 프로젝트 퇴근기록 확인
|
||||
const { data } = await $api.post('main/getUserLeaveRecord', {
|
||||
memberSeq: memberSeq,
|
||||
});
|
||||
|
||||
const res = data?.data;
|
||||
if (res && !res?.COMMUTLVE) {
|
||||
await projectStore.getMemberProjects();
|
||||
|
||||
if (projectStore.activeMemberProjectList.length > 0) {
|
||||
const previousProject =
|
||||
projectStore.activeMemberProjectList.find(p => res.MEMBERSEQ === user.value.id && res.PROJCTLVE === p.PROJCTSEQ) ||
|
||||
projectStore.activeMemberProjectList[0]; // 이전 출근 프로젝트가 없으면 첫 번째 프로젝트
|
||||
|
||||
if (previousProject) {
|
||||
selectedProject.value = previousProject.PROJCTSEQ;
|
||||
projectStore.setSelectedProject(previousProject);
|
||||
} else if (projectStore.activeProjectList.length > 0) {
|
||||
selectedProject.value = projectStore.activeProjectList[0].PROJCTSEQ;
|
||||
projectStore.setSelectedProject(projectStore.activeProjectList[0]);
|
||||
} else {
|
||||
selectedProject.value = null;
|
||||
projectStore.setSelectedProject(null);
|
||||
}
|
||||
} else {
|
||||
selectedProject.value = null;
|
||||
projectStore.setSelectedProject(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await userStore.userInfo();
|
||||
user.value = userStore.user;
|
||||
@ -673,6 +570,12 @@
|
||||
param.append('month', month);
|
||||
param.append('day', day);
|
||||
|
||||
// if (!weatherStore.dailyWeatherList?.length) {
|
||||
// await weatherStore.getWeatherInfo();
|
||||
// //dailyWeatherList.value = weatherStore.dailyWeatherList;
|
||||
// console.log('dailyWeatherList.value: ', dailyWeatherList.value);
|
||||
// }
|
||||
|
||||
// 이벤트 카테고리 호출
|
||||
await fetchCategoryList();
|
||||
await fetchEventList(param);
|
||||
@ -681,11 +584,6 @@
|
||||
// 스크롤 감지 이벤트 리스너
|
||||
window.addEventListener('wheel', handleWheelEvent);
|
||||
window.addEventListener('click', colorToday);
|
||||
|
||||
datePickerStore.initDatePicker(fullCalendarRef, async (year, month, options) => {
|
||||
// 데이터 다시 불러오기
|
||||
await fetchData();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@ -25,17 +25,19 @@
|
||||
<div class="avatar flex-shrink-0 me-1">
|
||||
<img
|
||||
style="cursor: auto;"
|
||||
class="rounded-circle user-avatar object-fit-cover"
|
||||
class="rounded-circle user-avatar"
|
||||
:src="getProfileImage(item.localVote.MEMBERPRF)"
|
||||
alt="최초 작성자"
|
||||
:style="{ borderColor: item.localVote.usercolor }"
|
||||
@error="setDefaultImage"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="timeline-event ps-1" style="cursor: pointer;" @click="goVoteList()" > -->
|
||||
<div class="timeline-event ps-1" style="cursor: pointer;" @click.stop="openModal(item.localVote.LOCVOTSEQ)" >
|
||||
<div class="timeline-header ">
|
||||
<small ><strong>{{ truncateTitle(item.localVote.LOCVOTTTL) }}</strong></small>
|
||||
</div>
|
||||
|
||||
<small class="d-flex align-items-center lh-1 me-4 mb-4 mb-sm-0"
|
||||
:style="{ color: getDaysAgo(item.localVote.formatted_LOCVOTEDT) == '금일 종료' ? 'red' : '' }">
|
||||
⏰{{getDaysAgo(item.localVote.formatted_LOCVOTEDT)}}({{item.localVote.total_voted}}/{{ item.localVote.total_votable }})
|
||||
@ -51,6 +53,7 @@
|
||||
<div class="card-body" v-else>
|
||||
진행중인 투표가 없습니다.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--투표 모달 -->
|
||||
@ -181,34 +184,30 @@ const addContents = (itemList, voteId) => {
|
||||
}
|
||||
//투표종료
|
||||
const endVoteId = (endVoteId) => {
|
||||
if(confirm('투표를 종료하시겠습니까?')){
|
||||
$api.patch('vote/updateEndData',{
|
||||
endVoteId :endVoteId
|
||||
}).then((res)=>{
|
||||
if(res.data.status === 'OK'){
|
||||
getvoteList();
|
||||
isModalOpen.value = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
$api.patch('vote/updateEndData',{
|
||||
endVoteId :endVoteId
|
||||
}).then((res)=>{
|
||||
if(res.data.status === 'OK'){
|
||||
getvoteList();
|
||||
isModalOpen.value = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
//투표 삭제
|
||||
const voteDelete =(id) =>{
|
||||
if(confirm('투표를 삭제하시겠습니까?')){
|
||||
$api.patch('vote/updateDeleteData',{
|
||||
deleteVoteId :id
|
||||
}).then((res)=>{
|
||||
if(res.data.status === 'OK'){
|
||||
toastStore.onToast('투표가 삭제되었습니다.', 's');
|
||||
getvoteList();
|
||||
isModalOpen.value = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
$api.patch('vote/updateDeleteData',{
|
||||
deleteVoteId :id
|
||||
}).then((res)=>{
|
||||
if(res.data.status === 'OK'){
|
||||
toastStore.onToast('투표가 삭제되었습니다.', 's');
|
||||
getvoteList();
|
||||
isModalOpen.value = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
// 제목이 14글자 넘어가면 ... 처리하는 함수
|
||||
const truncateTitle = title => {
|
||||
return title.length > 10 ? title.slice(0, 10) + '...' : title;
|
||||
return title.length > 10 ? title.slice(0, 10) + '...' : title;
|
||||
};
|
||||
|
||||
//투표이동
|
||||
@ -226,11 +225,13 @@ const goVoteList = () =>{
|
||||
const getDaysAgo = (dateString) => {
|
||||
const inputDate = new Date(dateString); // 문자열을 Date 객체로 변환
|
||||
const today = new Date(); // 현재 날짜 가져오기
|
||||
|
||||
const input = new Date(inputDate.getFullYear(), inputDate.getMonth(), inputDate.getDate());
|
||||
const now = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
|
||||
const timeDiff = now - input;
|
||||
const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
|
||||
// 오늘 날짜인 경우
|
||||
// 오늘 날짜인 경우 "오늘" 반환
|
||||
if (dayDiff === 0) return "금일 종료";
|
||||
|
||||
return `종료 ${Math.abs(dayDiff)}일 전`;
|
||||
@ -240,5 +241,6 @@ const getDaysAgo = (dateString) => {
|
||||
<style scoped>
|
||||
.user-avatar {
|
||||
border: 3px solid;
|
||||
padding: 0.1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
<!-- 프로필 이미지 -->
|
||||
<div class="avatar flex-shrink-0 me-2 d-flex align-items-center">
|
||||
<img
|
||||
class="rounded-circle user-avatar object-fit-cover"
|
||||
class="rounded-circle user-avatar"
|
||||
:src="getProfileImage(item.lastEditor.profileImage)"
|
||||
alt="최종 작성자"
|
||||
:style="{ borderColor: item.lastEditor.color }"
|
||||
@ -135,5 +135,6 @@ return title.length > 25 ? title.slice(0, 25) + '...' : title;
|
||||
<style scoped>
|
||||
.user-avatar {
|
||||
border: 3px solid;
|
||||
padding: 0.1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -50,15 +50,14 @@
|
||||
<label class="switch"
|
||||
><input
|
||||
type="checkbox"
|
||||
:checked="member.checked"
|
||||
@click="handleRegisterMember($event, member)" />
|
||||
<span class="slider round"></span
|
||||
:checked="checked"
|
||||
@change="handleRegisterMember(member.MEMBERSEQ)" /><span class="slider round"></span
|
||||
></label>
|
||||
</div>
|
||||
<button
|
||||
class="btn-close btn-close-sm"
|
||||
style="position: absolute; top: 10px; right: 10px"
|
||||
@click="handleRejectMember(member)"
|
||||
@click="handleRejectMember(member.MEMBERSEQ)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,37 +76,30 @@
|
||||
import $api from '@api';
|
||||
|
||||
const memberList = ref([]);
|
||||
const checked = ref(false);
|
||||
const toast = useToastStore();
|
||||
const imgURL = import.meta.env.VITE_SERVER_IMG_URL;
|
||||
|
||||
// 조회 api
|
||||
const fetchRegisterMemberList = async () => {
|
||||
const { data } = await $api.get('main/registerMemberList');
|
||||
if (data?.data) {
|
||||
memberList.value = data.data.map(member => ({
|
||||
...member,
|
||||
checked: false, // 각 항목에 checked 값 설정
|
||||
}));
|
||||
}
|
||||
if (data?.data) memberList.value = data.data;
|
||||
};
|
||||
|
||||
// 사원 등록 api
|
||||
const handleRegisterMember = async (e, member) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { data } = await $api.post('main/registerMember', { memberSeq: member.MEMBERSEQ });
|
||||
const handleRegisterMember = async memberSeq => {
|
||||
const { data } = await $api.post('main/registerMember', { memberSeq: memberSeq });
|
||||
if (data?.data) {
|
||||
member.checked = true;
|
||||
toast.onToast(data.data, 's');
|
||||
fetchRegisterMemberList();
|
||||
}
|
||||
};
|
||||
|
||||
// 사원 등록 미승인 api
|
||||
const handleRejectMember = async member => {
|
||||
const handleRejectMember = async memberSeq => {
|
||||
if (!confirm('해당 사원 등록을 거절하시겠습니까?')) return;
|
||||
|
||||
const { data } = await $api.post('main/rejectMember', { memberSeq: member.MEMBERSEQ });
|
||||
const { data } = await $api.post('main/rejectMember', { memberSeq: memberSeq });
|
||||
if (data?.data) {
|
||||
toast.onToast(data.data, 's');
|
||||
fetchRegisterMemberList();
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
<div class="text-center">
|
||||
<label
|
||||
for="profilePic"
|
||||
class="rounded-circle m-auto ui-bg-cover position-relative cursor-pointer "
|
||||
class="rounded-circle m-auto ui-bg-cover position-relative cursor-pointer"
|
||||
id="profileLabel"
|
||||
style="width: 100px; height: 100px; background-image: url(img/avatars/default-Profile.jpg); background-repeat: no-repeat; background-size: cover;"
|
||||
style="width: 100px; height: 100px; background-image: url(img/avatars/default-Profile.jpg); background-repeat: no-repeat"
|
||||
>
|
||||
</label>
|
||||
|
||||
@ -25,7 +25,6 @@
|
||||
@update:alert="idAlert = $event"
|
||||
@blur="checkIdDuplicate"
|
||||
:value="id"
|
||||
@keypress="noSpace"
|
||||
/>
|
||||
<span v-if="idError" class="invalid-feedback d-block">{{ idError }}</span>
|
||||
|
||||
@ -38,7 +37,6 @@
|
||||
@update:data="password = $event"
|
||||
@update:alert="passwordAlert = $event"
|
||||
:value="password"
|
||||
@keypress="noSpace"
|
||||
/>
|
||||
<span v-if="passwordError" class="invalid-feedback d-block">{{ passwordError }}</span>
|
||||
|
||||
@ -51,7 +49,6 @@
|
||||
@update:data="passwordcheck = $event"
|
||||
@update:alert="passwordcheckAlert = $event"
|
||||
:value="passwordcheck"
|
||||
@keypress="noSpace"
|
||||
/>
|
||||
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
|
||||
|
||||
@ -85,7 +82,6 @@
|
||||
@update:data="name = $event"
|
||||
@update:alert="nameAlert = $event"
|
||||
:value="name"
|
||||
@keypress="noSpace"
|
||||
class="me-2 w-50"
|
||||
/>
|
||||
|
||||
@ -218,10 +214,6 @@
|
||||
|
||||
const toastStore = useToastStore();
|
||||
|
||||
const noSpace = (e) => {
|
||||
if (e.key === ' ') e.preventDefault();
|
||||
};
|
||||
|
||||
// 프로필 체크
|
||||
const profileValid = (size, type) => {
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
@ -352,7 +344,6 @@
|
||||
});
|
||||
|
||||
watch(password, (newValue) => {
|
||||
|
||||
if (newValue && newValue.length >= 4) {
|
||||
passwordErrorAlert.value = false;
|
||||
passwordError.value = '';
|
||||
@ -405,10 +396,8 @@
|
||||
} else {
|
||||
passwordError.value = '';
|
||||
}
|
||||
const phoneRegex = /^010\d{8}$/;
|
||||
const isFormatValid = phoneRegex.test(phone.value);
|
||||
|
||||
if (!/^\d+$/.test(phone.value) || !isFormatValid) {
|
||||
if (!/^\d+$/.test(phone.value)) {
|
||||
phoneAlert.value = true;
|
||||
} else {
|
||||
phoneAlert.value = false;
|
||||
@ -445,13 +434,13 @@
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('memberIds', id.value.trim());
|
||||
formData.append('memberPwd', password.value.trim());
|
||||
formData.append('memberIds', id.value);
|
||||
formData.append('memberPwd', password.value);
|
||||
formData.append('memberPwh', pwhint.value);
|
||||
formData.append('memberPwr', pwhintRes.value.trim());
|
||||
formData.append('memberNam', name.value.trim());
|
||||
formData.append('memberPwr', pwhintRes.value);
|
||||
formData.append('memberNam', name.value);
|
||||
formData.append('memberArr', address.value);
|
||||
formData.append('memberDtl', detailAddress.value.trim());
|
||||
formData.append('memberDtl', detailAddress.value);
|
||||
formData.append('memberZip', postcode.value);
|
||||
formData.append('memberBth', birth.value);
|
||||
formData.append('memberTel', phone.value);
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<ul v-if="displayedUserList && displayedUserList.length > 0" class="list-unstyled users-list d-flex align-items-center gap-1 flex-wrap">
|
||||
<ul class="list-unstyled users-list d-flex align-items-center gap-1 flex-wrap">
|
||||
<li
|
||||
v-for="(user, index) in displayedUserList"
|
||||
:key="index"
|
||||
class="avatar pull-up "
|
||||
class="avatar pull-up"
|
||||
:class="{ 'opacity-100': isUserDisabled(user) }"
|
||||
@click.stop="showOnlyActive ? null : toggleDisable(index)"
|
||||
:style="showOnlyActive ? 'cursor: default' : ''"
|
||||
@ -14,7 +14,7 @@
|
||||
:data-bs-original-title="getTooltipTitle(user)"
|
||||
>
|
||||
<img
|
||||
class="user-avatar border border-3 rounded-circle object-fit-cover"
|
||||
class="user-avatar border border-3 rounded-circle object-fit-contain"
|
||||
:class="{ 'grayscaleImg': isUserDisabled(user) }"
|
||||
:src="`${baseUrl}upload/img/profile/${user.MEMBERPRF}`"
|
||||
:style="`border-color: ${user.usercolor} !important;`"
|
||||
@ -23,12 +23,12 @@
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<span v-else >-</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, nextTick, computed, watch } from 'vue';
|
||||
import { useUserStore } from '@s/userList';
|
||||
import { useProjectStore } from '@s/useProjectStore';
|
||||
import $api from '@api';
|
||||
import { useToastStore } from "@s/toastStore";
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
class="start-50 translate-middle crown-icon"
|
||||
/>
|
||||
<img
|
||||
class="rounded-circle object-fit-cover"
|
||||
class="rounded-circle profile-img"
|
||||
:src="getUserProfileImage(user.MEMBERPRF)"
|
||||
alt="user"
|
||||
:style="getDynamicStyle(user)"
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<h5 class="card-title mb-1">
|
||||
<div class="list-unstyled users-list d-flex align-items-center gap-1">
|
||||
<img
|
||||
class="object-fit-cover rounded-circle user-avatar border border-3 w-px-40 h-px-40"
|
||||
class="rounded-circle user-avatar border border-3 w-px-40 h-px-40"
|
||||
:src="`${baseUrl}upload/img/profile/${data.localVote.MEMBERPRF}`"
|
||||
:style="`border-color: ${data.localVote.usercolor} !important;`"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
:aria-label="data.MEMBERSEQ"
|
||||
:data-bs-original-title="getTooltipTitle(data)">
|
||||
<img
|
||||
class="rounded-circle user-avatar border border-3 object-fit-cover"
|
||||
class="rounded-circle user-avatar border border-3"
|
||||
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
|
||||
:style="`border-color: ${data.usercolor} !important; width: 90%; height: 90%;`"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
:aria-label="data.MEMBERSEQ"
|
||||
:data-bs-original-title="getTooltipTitle(data)">
|
||||
<img
|
||||
class="rounded-circle user-avatar border border-3 object-fit-cover"
|
||||
class="rounded-circle user-avatar border border-3"
|
||||
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
|
||||
:style="`border-color: ${data.usercolor} !important; width: 90%; height: 90%;`"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
<div class="d-flex flex-wrap align-items-center me-4">
|
||||
<div class="avatar me-2">
|
||||
<img
|
||||
class="rounded-circle user-avatar object-fit-cover"
|
||||
class="rounded-circle user-avatar"
|
||||
:src="getProfileImage(item.author.profileImage)"
|
||||
alt="최초 작성자"
|
||||
:style="{ borderColor: item.author.color }"
|
||||
@ -51,7 +51,7 @@
|
||||
>
|
||||
<div class="avatar me-2">
|
||||
<img
|
||||
class="rounded-circle user-avatar object-fit-cover"
|
||||
class="rounded-circle user-avatar"
|
||||
:src="getProfileImage(item.lastEditor.profileImage)"
|
||||
alt="최근 작성자"
|
||||
:style="{ borderColor: item.lastEditor.color }"
|
||||
@ -164,6 +164,7 @@ const toggleEdit = async () => {
|
||||
|
||||
.user-avatar {
|
||||
border: 3px solid;
|
||||
padding: 0.1px;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
@ -181,18 +182,4 @@ const toggleEdit = async () => {
|
||||
.btn.btn-primary {
|
||||
writing-mode: horizontal-tb;
|
||||
}
|
||||
.dict-content-wrap {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dict-content-wrap * {
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
word-break: break-word;
|
||||
white-space: normal !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
@keyup="ValidHandler('title')"
|
||||
/>
|
||||
<div>
|
||||
<QEditor class="q-editor-container" @keyup="ValidHandler('content')" @update:data="handleContentUpdate" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" :initialData="contentValue"/>
|
||||
<QEditor class="q-editor-container" @keyup="ValidHandler('content')" @update:data="handleContentUpdate" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" :initialData="contentValue"/>
|
||||
<div class="text-end mt-5">
|
||||
<button class="btn btn-primary" @click="saveWord" :disabled="titleValue ? !changed : false">
|
||||
<i class="bx bx-check"></i>
|
||||
@ -233,10 +233,10 @@ const handleCategoryFocusout = (value) => {
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.q-editor-container * {
|
||||
max-width: 100% !important;
|
||||
word-break: break-all !important;
|
||||
box-sizing: border-box;
|
||||
white-space: normal !important;
|
||||
.q-editor-container {
|
||||
max-width: 100%; /* 영역이 넘치지 않게 */
|
||||
overflow: auto; /* 넘치는 내용은 스크롤로 처리 */
|
||||
word-wrap: break-word; /* 긴 단어는 자동으로 줄바꿈 */
|
||||
white-space: normal; /* 내용이 길어지면 자동으로 줄바꿈 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -80,12 +80,12 @@
|
||||
<div class="text-truncate">Authorization</div>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="menu-item" :class="$route.path.includes('/people') ? 'active' : ''">
|
||||
<RouterLink class="menu-link" to="/people"> <i class="bi "></i>
|
||||
<i class="menu-icon icon-base bi bi-people-fill"></i>
|
||||
<div class="text-truncate">people</div>
|
||||
<!-- <li class="menu-item" :class="$route.path.includes('/sample') ? 'active' : ''">
|
||||
<RouterLink class="menu-link" to="/sample"> <i class="bi "></i>
|
||||
<i class="menu-icon tf-icons bx bx-calendar"></i>
|
||||
<div class="text-truncate">Sample</div>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</li> -->
|
||||
</ul>
|
||||
</aside>
|
||||
<!-- / Menu -->
|
||||
@ -94,7 +94,6 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
const userStore = useUserInfoStore();
|
||||
const allowedUserId = 1; // 특정 ID (변경필요!!)
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
<img v-if="weather.icon" :src="customIconUrl" :alt="weather.description" :class="customIconClass" />
|
||||
<div class="d-flex flex-column">
|
||||
<span class="weather-desc">{{ weather.description }}</span>
|
||||
<span class="weather-temp" v-if="weatherReady">
|
||||
<span class="weather-temp" v-if="weather.tempMin !== null && weather.tempMax !== null">
|
||||
최저 {{ weather.tempMin }}° / 최고 {{ weather.tempMax }}°
|
||||
</span>
|
||||
</div>
|
||||
@ -38,36 +38,135 @@
|
||||
<!-- Notification -->
|
||||
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2 p-0">
|
||||
<a
|
||||
class="nav-link dropdown-toggle hide-arrow p-0"
|
||||
href="javascript:void(0);"
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-auto-close="outside"
|
||||
aria-expanded="false"
|
||||
class="nav-link dropdown-toggle hide-arrow p-0"
|
||||
href="javascript:void(0);"
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-auto-close="outside"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="position-relative">
|
||||
<i class="bx bx-bell bx-md"></i>
|
||||
<!-- 알림이 있을 경우에만 뱃지를 표시 -->
|
||||
<span
|
||||
v-if="notificationCount > 0"
|
||||
class="badge rounded-pill bg-danger badge-dot badge-notifications border"
|
||||
></span>
|
||||
</span>
|
||||
<span class="position-relative">
|
||||
<i class="bx bx-bell bx-md"></i>
|
||||
<span class="badge rounded-pill bg-danger badge-dot badge-notifications border"></span>
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end p-0">
|
||||
<li class="dropdown-notifications-list scrollable-container p-3">
|
||||
<!-- 알림이 없으면 "알림이 없습니다." 메시지 표시 -->
|
||||
<div v-if="notificationCount === 0">
|
||||
알림이 없습니다.
|
||||
</div>
|
||||
<!-- 알림이 있을 때 목록 렌더링-->
|
||||
<div v-else>
|
||||
<ul>
|
||||
<li v-for="notification in notifications" :key="notification.id">
|
||||
{{ notification.text }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-menu-header border-bottom">
|
||||
<div class="dropdown-header d-flex align-items-center py-3">
|
||||
<h6 class="mb-0 me-auto">Notification</h6>
|
||||
<div class="d-flex align-items-center h6 mb-0">
|
||||
<span class="badge bg-label-primary me-2">8 New</span>
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="dropdown-notifications-all p-2"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="Mark all as read"
|
||||
><i class="bx bx-envelope-open text-heading"></i
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown-notifications-list scrollable-container">
|
||||
<ul class="list-group list-group-flush">
|
||||
<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/1.png" class="rounded-circle" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="small mb-0">Congratulation Lettie 🎉</h6>
|
||||
<small class="mb-1 d-block text-body">Won the monthly best seller gold badge</small>
|
||||
<small class="text-muted">1h 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">
|
||||
<span class="avatar-initial rounded-circle bg-label-danger">CF</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="small mb-0">Charles Franklin</h6>
|
||||
<small class="mb-1 d-block text-body">Accepted your connection</small>
|
||||
<small class="text-muted">12hr 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/2.png" class="rounded-circle" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="small mb-0">New Message ✉️</h6>
|
||||
<small class="mb-1 d-block text-body">You have new message from Natalie</small>
|
||||
<small class="text-muted">1h 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">
|
||||
<span class="avatar-initial rounded-circle bg-label-success"
|
||||
><i class="bx bx-cart"></i
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="small mb-0">Whoo! You have new order 🛒</h6>
|
||||
<small class="mb-1 d-block text-body">ACME Inc. made new order $1,154</small>
|
||||
<small class="text-muted">1 day 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">
|
||||
<div class="d-grid p-4">
|
||||
<a class="btn btn-primary btn-sm d-flex" href="javascript:void(0);">
|
||||
<small class="align-middle">View all notifications</small>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<!--/ Notification -->
|
||||
@ -78,7 +177,7 @@
|
||||
v-if="user"
|
||||
:src="`${baseUrl}upload/img/profile/${user.profile}`"
|
||||
alt="Profile Image"
|
||||
class="w-px-40 h-px-40 rounded-circle border border-3 object-fit-cover"
|
||||
class="w-px-40 h-px-40 rounded-circle border border-3 object-fit-contain"
|
||||
:style="`border-color: ${user.usercolor} !important;`"
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
/>
|
||||
@ -131,17 +230,6 @@
|
||||
const selectedProject = ref(null);
|
||||
const weather = ref({});
|
||||
const dailyWeatherList = ref([]);
|
||||
const notifications = ref([]);
|
||||
const notificationCount = ref(0);
|
||||
|
||||
const weatherReady = computed(() => {
|
||||
return (
|
||||
weather.value &&
|
||||
weather.value.tempMin !== null &&
|
||||
weather.value.tempMax !== null &&
|
||||
!!weather.value.description
|
||||
);
|
||||
});
|
||||
|
||||
// 내가 참여하고 있는 진행 중인 프로젝트 목록
|
||||
const myActiveProjects = computed(() => {
|
||||
@ -195,6 +283,8 @@
|
||||
return 'weather-icon';
|
||||
});
|
||||
|
||||
const weatherKorean = computed(() => weather.value.description || '날씨 정보 없음');
|
||||
|
||||
// const { isDarkMode, switchToDarkMode, switchToLightMode } = useThemeStore();
|
||||
|
||||
const handleLogout = async () => {
|
||||
@ -230,7 +320,7 @@
|
||||
// 로그인 페이지가 아닐 때만 날씨 정보를 가져오도록
|
||||
if (route.name !== 'login' && route.name !== undefined) {
|
||||
// 날씨 정보 갱신
|
||||
await weatherStore.getWeatherInfoWithCache();
|
||||
await weatherStore.getWeatherInfo();
|
||||
weather.value = weatherStore.weather; // 오늘 날씨
|
||||
dailyWeatherList.value = weatherStore.dailyWeatherList; // 주간 날씨
|
||||
}
|
||||
@ -251,35 +341,26 @@
|
||||
color: #888;
|
||||
line-height: 1.2;
|
||||
}
|
||||
/* .weather-box {
|
||||
.weather-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
max-width: 3000px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
} */
|
||||
.custom-sunny-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.weather-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
gap: 10px;
|
||||
min-width: 160px; /* 필요시 */
|
||||
.custom-sunny-icon {
|
||||
width: 5%;
|
||||
height: auto;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.custom-sunny-icon {
|
||||
width: 40px;
|
||||
width: 6%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.custom-sunny-icon {
|
||||
width: 30px;
|
||||
width: 14%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -22,7 +22,6 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: 'write',
|
||||
name: 'BoardWrite',
|
||||
component: () => import('@v/board/BoardWrite.vue'),
|
||||
},
|
||||
{
|
||||
@ -45,7 +44,6 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: '/wordDict',
|
||||
name: 'WordDict',
|
||||
component: () => import('@v/wordDict/wordDict.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
@ -75,46 +73,34 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: '/voteboard',
|
||||
name: 'VoteBoard',
|
||||
component: () => import('@v/voteboard/TheVoteBoard.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'VoteBoardList',
|
||||
component: () => import('@v/voteboard/voteBoardList.vue'),
|
||||
},
|
||||
{
|
||||
path: 'write',
|
||||
name: 'VoteboardWrite',
|
||||
component: () => import('@v/voteboard/voteboardWrite.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/projectlist',
|
||||
name: 'Projectlist',
|
||||
component: () => import('@v/projectlist/TheProjectList.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/commuters',
|
||||
name: 'Commuters',
|
||||
component: () => import('@v/commuters/TheCommuters.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/authorization',
|
||||
name: 'Authorization',
|
||||
component: () => import('@v/admin/TheAuthorization.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/people',
|
||||
name: 'people',
|
||||
component: () => import('@v/people/PeopleList.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/error/400',
|
||||
name: 'Error400',
|
||||
@ -154,7 +140,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
|
||||
// Authorization 페이지는 ID가 26이 아니면 접근 차단
|
||||
if (to.path === '/authorization' && userId !== allowedUserId) {
|
||||
return next();
|
||||
return next('/');
|
||||
}
|
||||
|
||||
// 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
/*
|
||||
작성자 : 서지희
|
||||
작성일 : 2025-04-04
|
||||
수정일 : 2025-04-07
|
||||
설명 : 위치 기반으로 날씨를 조회하고, 10분 단위 캐시로 저장합니다.
|
||||
수정자 :
|
||||
수정일 :
|
||||
설명 : 위치 기반으로 날씨를 조회하고, 오늘의 최저/최고 기온과 현재 날씨 아이콘/설명을 저장합니다.
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
@ -19,104 +20,66 @@ export const useWeatherStore = defineStore('weather', () => {
|
||||
const dailyWeatherList = ref([]);
|
||||
|
||||
const getWeatherInfo = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(async position => {
|
||||
const lat = position.coords.latitude;
|
||||
const lon = position.coords.longitude;
|
||||
navigator.geolocation.getCurrentPosition(async position => {
|
||||
const lat = position.coords.latitude;
|
||||
const lon = position.coords.longitude;
|
||||
|
||||
try {
|
||||
const res = await $api.get(`/weather`, {
|
||||
params: { lat, lon },
|
||||
withCredentials: true,
|
||||
});
|
||||
try {
|
||||
const res = await $api.get(`/weather`, {
|
||||
params: { lat, lon },
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
if (!res?.data?.data) return;
|
||||
if (!res?.data?.data) return;
|
||||
|
||||
const resData = res.data.data;
|
||||
const raw = resData.weatherInfo;
|
||||
const data = JSON.parse(raw);
|
||||
if (!data || !Array.isArray(data.list)) {
|
||||
console.error('날씨 데이터 형식 오류');
|
||||
return;
|
||||
}
|
||||
|
||||
// 검은색 태양 아이콘 변경
|
||||
dailyWeatherList.value = resData.dailyWeatherList.map(w => {
|
||||
return {
|
||||
...w,
|
||||
icon: w.icon.replace(/n$/, 'd'),
|
||||
};
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const todayStr = now.toISOString().split('T')[0];
|
||||
const nowTime = now.getTime();
|
||||
|
||||
const todayList = data.list.filter(item => item.dt_txt.startsWith(todayStr));
|
||||
if (todayList.length > 0) {
|
||||
const minTemp = Math.min(...todayList.map(i => i.main.temp_min));
|
||||
const maxTemp = Math.max(...todayList.map(i => i.main.temp_max));
|
||||
weather.value.tempMin = Math.round(minTemp);
|
||||
weather.value.tempMax = Math.round(maxTemp);
|
||||
}
|
||||
|
||||
const closest = data.list.reduce((prev, curr) => {
|
||||
const prevDiff = Math.abs(new Date(prev.dt_txt).getTime() - nowTime);
|
||||
const currDiff = Math.abs(new Date(curr.dt_txt).getTime() - nowTime);
|
||||
return currDiff < prevDiff ? curr : prev;
|
||||
});
|
||||
|
||||
weather.value.icon = closest.weather[0].icon.replace(/n$/, 'd');
|
||||
weather.value.description = closest.weather[0].description;
|
||||
|
||||
resolve({ weather: weather.value, dailyWeatherList: dailyWeatherList.value });
|
||||
} catch (e) {
|
||||
console.error('날씨 정보 가져오기 실패:', e);
|
||||
reject(e);
|
||||
const resData = res.data.data;
|
||||
const raw = resData.weatherInfo;
|
||||
const data = JSON.parse(raw);
|
||||
if (!data || !Array.isArray(data.list) || data.list.length === 0) {
|
||||
console.error('날씨 데이터 형식 오류 또는 없음:', data);
|
||||
return;
|
||||
}
|
||||
}, reject);
|
||||
});
|
||||
};
|
||||
|
||||
const getWeatherInfoWithCache = async () => {
|
||||
const now = new Date();
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
const key = `weather_${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(Math.floor(now.getMinutes() / 10) * 10)}`;
|
||||
const cached = localStorage.getItem(key);
|
||||
if (cached) {
|
||||
const parsed = JSON.parse(cached);
|
||||
weather.value = parsed.weather;
|
||||
dailyWeatherList.value = parsed.dailyWeatherList;
|
||||
return;
|
||||
}
|
||||
// 주간 예보 리스트 저장
|
||||
dailyWeatherList.value = resData.dailyWeatherList;
|
||||
|
||||
try {
|
||||
const { weather: w, dailyWeatherList: d } = await getWeatherInfo();
|
||||
const now = new Date();
|
||||
const nowTime = now.getTime();
|
||||
const todayStr = now.toISOString().split('T')[0];
|
||||
|
||||
// 기존 캐시 삭제
|
||||
Object.keys(localStorage).forEach(k => {
|
||||
if (k.startsWith('weather_')) localStorage.removeItem(k);
|
||||
});
|
||||
// 오늘의 데이터만 필터링
|
||||
const todayList = data.list.filter(item => item.dt_txt.startsWith(todayStr));
|
||||
|
||||
localStorage.setItem(key, JSON.stringify({ weather: w, dailyWeatherList: d }));
|
||||
} catch (e) {
|
||||
console.error('날씨 API 호출 실패, 캐시 fallback 시도 중...');
|
||||
const oldKey = Object.keys(localStorage)
|
||||
.filter(k => k.startsWith('weather_'))
|
||||
.sort()
|
||||
.pop();
|
||||
if (oldKey) {
|
||||
const fallback = JSON.parse(localStorage.getItem(oldKey));
|
||||
weather.value = fallback.weather;
|
||||
dailyWeatherList.value = fallback.dailyWeatherList;
|
||||
if (todayList.length > 0) {
|
||||
// 오늘의 최저 / 최고 기온 계산
|
||||
const minTemp = Math.min(...todayList.map(i => i.main.temp_min));
|
||||
const maxTemp = Math.max(...todayList.map(i => i.main.temp_max));
|
||||
weather.value.tempMin = Math.round(minTemp);
|
||||
weather.value.tempMax = Math.round(maxTemp);
|
||||
} else {
|
||||
weather.value.tempMin = null;
|
||||
weather.value.tempMax = null;
|
||||
}
|
||||
|
||||
// 현재 시간과 가장 가까운 시간대 데이터 추출 (아이콘 및 설명용)
|
||||
const closest = data.list.reduce((prev, curr) => {
|
||||
const prevDiff = Math.abs(new Date(prev.dt_txt).getTime() - nowTime);
|
||||
const currDiff = Math.abs(new Date(curr.dt_txt).getTime() - nowTime);
|
||||
return currDiff < prevDiff ? curr : prev;
|
||||
});
|
||||
|
||||
weather.value.icon = closest.weather[0].icon.replace(/n$/, 'd');
|
||||
weather.value.description = closest.weather[0].description;
|
||||
|
||||
} catch (e) {
|
||||
console.error('날씨 정보 가져오기 실패:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
weather,
|
||||
dailyWeatherList,
|
||||
getWeatherInfo,
|
||||
getWeatherInfoWithCache,
|
||||
};
|
||||
});
|
||||
|
||||
@ -21,18 +21,18 @@
|
||||
import MainVote from '@c/main/MainVote.vue';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import $api from '@api';
|
||||
|
||||
const userStore = useUserInfoStore();
|
||||
const user = ref();
|
||||
const isAdmin = ref(false);
|
||||
|
||||
const checkAdmin = async user => {
|
||||
const { data } = await $api.post('user/authCheck', { memberId: user.loginId });
|
||||
return data.data === 'ROLE_ADMIN' ? true : false;
|
||||
const checkAdmin = user => {
|
||||
return user?.value?.role === 'ROLE_ADMIN' ? true : false;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await userStore.userInfo();
|
||||
isAdmin.value = await checkAdmin(userStore.user);
|
||||
user.value = userStore.user;
|
||||
isAdmin.value = await checkAdmin(user);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,35 +1,26 @@
|
||||
<template>
|
||||
<div class="container text-center flex-grow-1 container-p-y">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex flex-column">
|
||||
<h3>관리자 권한 부여</h3>
|
||||
<div class="user-card-container">
|
||||
<div v-for="user in users" :key="user.id" class="user-card">
|
||||
<!-- 프로필 사진 -->
|
||||
<img
|
||||
:src="getProfileImage(user.photo)"
|
||||
class="user-avatar2"
|
||||
alt="프로필 사진"
|
||||
@error="setDefaultImage"
|
||||
/>
|
||||
<!-- 사용자 정보 -->
|
||||
<div class="user-info">
|
||||
<h5>{{ user.name }}</h5>
|
||||
<div class="container text-center flex-grow-1 container-p-y">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex flex-column">
|
||||
<h3>관리자 권한 부여</h3>
|
||||
<div class="user-card-container">
|
||||
<div v-for="user in users" :key="user.id" class="user-card">
|
||||
<!-- 프로필 사진 -->
|
||||
<img :src="getProfileImage(user.photo)" class="profile-img" alt="프로필 사진" @error="setDefaultImage" />
|
||||
<!-- 사용자 정보 -->
|
||||
<div class="user-info">
|
||||
<h5>{{ user.name }}</h5>
|
||||
</div>
|
||||
<!-- 권한 토글 버튼 -->
|
||||
<label class="switch me-0">
|
||||
<input type="checkbox" :checked="user.isAdmin" @change="toggleAdmin(user)" />
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 권한 토글 버튼 (기본 동작 막고 클릭시 직접 토글 처리) -->
|
||||
<label class="switch me-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="user.isAdmin"
|
||||
@click="handleToggle($event, user)"
|
||||
/>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -41,80 +32,65 @@ const users = ref([]);
|
||||
const toastStore = useToastStore();
|
||||
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, "");
|
||||
const defaultProfile = "/img/icons/icon.png";
|
||||
const allowedUserId = 1; // 특정 ID (필요에 따라 변경)
|
||||
|
||||
const allowedUserId = 1; // 특정 ID (변경필요!!)
|
||||
// 사용자 목록 가져오기
|
||||
async function fetchUsers() {
|
||||
try {
|
||||
const response = await axios.get('admin/users'); // API 경로 확인
|
||||
if (!response.data || !Array.isArray(response.data.data)) {
|
||||
throw new Error("올바른 데이터 형식이 아닙니다.");
|
||||
try {
|
||||
const response = await axios.get('admin/users'); // API 경로 확인 필요
|
||||
|
||||
// API 응답 구조 확인 후 데이터가 배열인지 체크
|
||||
if (!response.data || !Array.isArray(response.data.data)) {
|
||||
throw new Error("올바른 데이터 형식이 아닙니다.");
|
||||
}
|
||||
|
||||
// MEMBERSEQ가 1이 아닌 회원만 필터링하여 데이터 매핑
|
||||
users.value = response.data.data
|
||||
.filter(user => user.MEMBERSEQ !== allowedUserId) // MEMBERSEQ가 1이면 제외
|
||||
.map(user => ({
|
||||
id: user.MEMBERSEQ,
|
||||
name: user.MEMBERNAM,
|
||||
photo: user.MEMBERPRF ? `${baseUrl}upload/img/profile/${user.MEMBERPRF}` : defaultProfile,
|
||||
color: user.MEMBERCOL,
|
||||
isAdmin: user.MEMBERROL === 'ROLE_ADMIN',
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
toastStore.onToast('사용자 목록을 불러오지 못했습니다.', 'e');
|
||||
}
|
||||
users.value = response.data.data
|
||||
.filter(user => user.MEMBERSEQ !== allowedUserId)
|
||||
.map(user => ({
|
||||
id: user.MEMBERSEQ,
|
||||
name: user.MEMBERNAM,
|
||||
photo: user.MEMBERPRF ? `${baseUrl}upload/img/profile/${user.MEMBERPRF}` : defaultProfile,
|
||||
color: user.MEMBERCOL,
|
||||
isAdmin: user.MEMBERROL === 'ROLE_ADMIN',
|
||||
}));
|
||||
} catch (error) {
|
||||
toastStore.onToast('사용자 목록을 불러오지 못했습니다.', 'e');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로필 이미지 가져오기
|
||||
function getProfileImage(photo) {
|
||||
return photo || defaultProfile;
|
||||
return photo || defaultProfile;
|
||||
}
|
||||
|
||||
// 이미지 로드 오류 시 기본 이미지 설정
|
||||
function setDefaultImage(event) {
|
||||
event.target.src = defaultProfile;
|
||||
event.target.src = defaultProfile;
|
||||
}
|
||||
|
||||
// 권한 토글 시 기본 동작 막고 직접 제어하는 함수
|
||||
async function handleToggle(event, user) {
|
||||
// Prevent the default checkbox toggle behavior
|
||||
event.preventDefault();
|
||||
// 관리자 권한 토글 함수
|
||||
async function toggleAdmin(user) {
|
||||
const requestData = {
|
||||
id: user.id,
|
||||
role: user.isAdmin ? 'MEMBER' : 'ADMIN'
|
||||
};
|
||||
try {
|
||||
const response = await axios.put('admin/role', requestData);
|
||||
|
||||
// 저장: 현재 상태를 기반으로 변경 요청 (체크박스는 아직 변하지 않았음)
|
||||
const originalState = user.isAdmin;
|
||||
const newState = !originalState;
|
||||
|
||||
const requestData = {
|
||||
id: user.id,
|
||||
role: originalState ? 'MEMBER' : 'ADMIN'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.put('admin/role', requestData);
|
||||
if (response.status === 200) {
|
||||
// 서버 요청 성공 시에만 상태를 업데이트
|
||||
user.isAdmin = newState;
|
||||
toastStore.onToast(`'${user.name}'의 권한이 '${requestData.role}'(으)로 변경되었습니다.`, 's');
|
||||
} else {
|
||||
throw new Error('권한 변경 실패');
|
||||
if (response.status === 200) {
|
||||
user.isAdmin = !user.isAdmin;
|
||||
toastStore.onToast(`'${user.name}'의 권한이 '${requestData.role}'(으)로 변경되었습니다.`, 's');
|
||||
} else {
|
||||
throw new Error('권한 변경 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
toastStore.onToast('권한 변경에 실패했습니다.', 'e');
|
||||
}
|
||||
} catch (error) {
|
||||
// 에러 발생 시 상태를 변경하지 않음
|
||||
toastStore.onToast('권한 변경에 실패했습니다.', 'e');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchUsers);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-avatar2 {
|
||||
width: 160px;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
margin: 1rem auto 0 auto;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -99,7 +99,7 @@
|
||||
|
||||
// 상태 변수
|
||||
const title = ref('');
|
||||
const content = ref({ ops: [] });
|
||||
const content = ref('');
|
||||
const autoIncrement = ref(0);
|
||||
|
||||
// 경고 상태
|
||||
@ -130,9 +130,10 @@
|
||||
// 최초 업데이트 감지 여부
|
||||
const isFirstContentUpdate = ref(true);
|
||||
|
||||
// 에디터 데이터 업데이트 시 처리 (최초 데이터 저장)
|
||||
// 에디터에서 데이터 업데이트 시
|
||||
const handleEditorDataUpdate = data => {
|
||||
content.value = data;
|
||||
|
||||
if (isFirstContentUpdate.value) {
|
||||
originalContent.value = structuredClone(data);
|
||||
isFirstContentUpdate.value = false;
|
||||
@ -140,28 +141,23 @@
|
||||
}
|
||||
};
|
||||
|
||||
// isDeltaChanged 함수 수정 (내장 diff 대신 텍스트, 이미지, 비디오 각각을 직접 비교)
|
||||
function isDeltaChanged(current, original) {
|
||||
const Delta = Quill.import('delta');
|
||||
const currentDelta = new Delta(current || []);
|
||||
const originalDelta = new Delta(original || []);
|
||||
|
||||
// 텍스트 추출
|
||||
const diff = originalDelta.diff(currentDelta);
|
||||
if (!diff || diff.ops.length === 0) return false;
|
||||
|
||||
// 텍스트만 비교해서 완전 동일한지 확인
|
||||
const getPlainText = delta =>
|
||||
(delta.ops || [])
|
||||
.filter(op => typeof op.insert === 'string')
|
||||
.map(op => op.insert)
|
||||
.join('');
|
||||
// 이미지 URL 추출
|
||||
|
||||
const getImages = delta =>
|
||||
(delta.ops || [])
|
||||
.filter(op => typeof op.insert === 'object' && op.insert.image)
|
||||
.map(op => op.insert.image);
|
||||
// 비디오 URL 추출
|
||||
const getVideos = delta =>
|
||||
(delta.ops || [])
|
||||
.filter(op => typeof op.insert === 'object' && op.insert.video)
|
||||
.map(op => op.insert.video);
|
||||
(delta.ops || []).filter(op => typeof op.insert === 'object' && op.insert.image).map(op => op.insert.image);
|
||||
|
||||
const textCurrent = getPlainText(currentDelta);
|
||||
const textOriginal = getPlainText(originalDelta);
|
||||
@ -169,27 +165,22 @@
|
||||
const imgsCurrent = getImages(currentDelta);
|
||||
const imgsOriginal = getImages(originalDelta);
|
||||
|
||||
const vidsCurrent = getVideos(currentDelta);
|
||||
const vidsOriginal = getVideos(originalDelta);
|
||||
|
||||
const textEqual = textCurrent === textOriginal;
|
||||
const imageEqual = imgsCurrent.length === imgsOriginal.length && imgsCurrent.every((val, idx) => val === imgsOriginal[idx]);
|
||||
const videoEqual = vidsCurrent.length === vidsOriginal.length && vidsCurrent.every((val, idx) => val === vidsOriginal[idx]);
|
||||
const imageEqual = JSON.stringify(imgsCurrent) === JSON.stringify(imgsOriginal);
|
||||
|
||||
return !(textEqual && imageEqual && videoEqual);
|
||||
return !(textEqual && imageEqual); // 둘 다 같아야 false
|
||||
}
|
||||
|
||||
// 게시물 변경 여부 계산
|
||||
const isChanged = computed(() => {
|
||||
if (!contentInitialized.value) return false;
|
||||
const isTitleChanged = title.value !== originalTitle.value;
|
||||
const isContentChanged = isDeltaChanged(content.value, originalContent.value);
|
||||
const isFilesChanged =
|
||||
attachFiles.value.some(f => !f.id) || // 신규 파일 존재
|
||||
attachFiles.value.some(f => !f.id) || // id 없는 새 파일이 있는 경우
|
||||
delFileIdx.value.length > 0 || // 삭제된 파일이 있는 경우
|
||||
!isSameFiles(
|
||||
attachFiles.value.filter(f => f.id), // 기존 파일만 비교
|
||||
originalFiles.value
|
||||
attachFiles.value.filter(f => f.id), // 기존 파일(id 있는 것만)
|
||||
originalFiles.value,
|
||||
);
|
||||
return isTitleChanged || isContentChanged || isFilesChanged;
|
||||
});
|
||||
@ -197,8 +188,10 @@
|
||||
// 파일 비교 함수
|
||||
function isSameFiles(current, original) {
|
||||
if (current.length !== original.length) return false;
|
||||
|
||||
const sortedCurrent = [...current].sort((a, b) => a.id - b.id);
|
||||
const sortedOriginal = [...original].sort((a, b) => a.id - b.id);
|
||||
|
||||
return sortedCurrent.every((file, idx) => {
|
||||
return file.id === sortedOriginal[idx].id && file.name === sortedOriginal[idx].name;
|
||||
});
|
||||
@ -206,24 +199,31 @@
|
||||
|
||||
// 게시물 데이터 로드
|
||||
const fetchBoardDetails = async () => {
|
||||
// 수정 데이터 전송
|
||||
let password = accessStore.password;
|
||||
const params = {
|
||||
password: `${password}` || '',
|
||||
};
|
||||
//const response = await axios.get(`board/${currentBoardId.value}`);
|
||||
const { data } = await axios.post(`board/${currentBoardId.value}`, params);
|
||||
|
||||
if (data.code !== 200) {
|
||||
//toastStore.onToast(data.message, 'e');
|
||||
alert(data.message, 'e');
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
const boardData = data.data;
|
||||
// 기존 첨부파일 추가
|
||||
if (boardData.hasAttachment && boardData.attachments.length > 0) {
|
||||
const formatted = addDisplayFileName([...boardData.attachments]);
|
||||
attachFiles.value = formatted;
|
||||
originalFiles.value = formatted;
|
||||
}
|
||||
|
||||
// 데이터 설정
|
||||
title.value = boardData.title || '제목 없음';
|
||||
content.value = boardData.content || { ops: [] };
|
||||
content.value = boardData.content || '내용 없음';
|
||||
originalTitle.value = title.value;
|
||||
originalContent.value = structuredClone(boardData.content);
|
||||
contentInitialized.value = true;
|
||||
@ -242,34 +242,38 @@
|
||||
const addDisplayFileName = fileInfos =>
|
||||
fileInfos.map(file => ({
|
||||
...file,
|
||||
name: `${file.originalName}.${file.extension}`
|
||||
name: `${file.originalName}.${file.extension}`,
|
||||
}));
|
||||
|
||||
// 상세 페이지 이동
|
||||
// 상세 페이지로 이동
|
||||
const goList = () => {
|
||||
accessStore.$reset();
|
||||
|
||||
// 목록으로 바로 이동시 필터 유지
|
||||
// const getFilter = localStorage.getItem(`boardList_${currentBoardId.value}`);
|
||||
// if (getFilter) {
|
||||
// router.push({
|
||||
// path: '/board',
|
||||
// query: JSON.parse(getFilter),
|
||||
// });
|
||||
// } else {
|
||||
// router.push('/board');
|
||||
// }
|
||||
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 전 페이지 이동
|
||||
// 전 페이지로 이동
|
||||
const goBack = () => {
|
||||
accessStore.$reset();
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 로컬 유효성 검사 함수 (에디터 내용: 텍스트, 이미지, 비디오 중 하나라도 있으면 유효)
|
||||
const isNotValidContent = delta => {
|
||||
if (!delta?.ops?.length) return true;
|
||||
const hasText = delta.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
|
||||
const hasImage = delta.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
|
||||
const hasVideo = delta.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.video);
|
||||
return !(hasText || hasImage || hasVideo);
|
||||
};
|
||||
|
||||
// 유효성 확인 함수
|
||||
// 유효성 확인
|
||||
const checkValidation = () => {
|
||||
contentAlert.value = isNotValidContent(content.value);
|
||||
contentAlert.value = $common.isNotValidContent(content);
|
||||
titleAlert.value = $common.isNotValidInput(title.value);
|
||||
|
||||
if (titleAlert.value || contentAlert.value || !isFileValid.value) {
|
||||
if (titleAlert.value) {
|
||||
title.value = '';
|
||||
@ -285,6 +289,7 @@
|
||||
|
||||
const handleFileUpload = files => {
|
||||
const validFiles = files.filter(file => file.size <= maxSize);
|
||||
|
||||
if (files.some(file => file.size > maxSize)) {
|
||||
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
|
||||
return;
|
||||
@ -295,11 +300,13 @@
|
||||
}
|
||||
fileError.value = '';
|
||||
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
||||
|
||||
autoIncrement.value++;
|
||||
};
|
||||
|
||||
const removeFile = (index, file) => {
|
||||
if (file.id) delFileIdx.value.push(file.id);
|
||||
|
||||
attachFiles.value.splice(index, 1);
|
||||
if (attachFiles.value.length <= maxFiles) {
|
||||
fileError.value = '';
|
||||
@ -317,41 +324,55 @@
|
||||
};
|
||||
////////////////// fileSection[E] ////////////////////
|
||||
|
||||
/** content 변경 감지 (deep 옵션 추가) */
|
||||
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
||||
watch(content, () => {
|
||||
contentAlert.value = isNotValidContent(content.value);
|
||||
}, { deep: true });
|
||||
contentAlert.value = $common.isNotValidContent(content);
|
||||
});
|
||||
|
||||
// 글 제목 유효성 검사
|
||||
// 글 제목 유효성
|
||||
const validateTitle = () => {
|
||||
titleAlert.value = title.value.trim().length === 0;
|
||||
};
|
||||
|
||||
// 게시물 수정 함수
|
||||
// 게시물 수정
|
||||
const updateBoard = async () => {
|
||||
if (checkValidation()) return;
|
||||
|
||||
// 수정 데이터 전송
|
||||
const boardData = {
|
||||
LOCBRDTTL: title.value.trim(),
|
||||
LOCBRDCON: JSON.stringify(content.value),
|
||||
LOCBRDSEQ: currentBoardId.value
|
||||
LOCBRDSEQ: currentBoardId.value,
|
||||
};
|
||||
|
||||
// 업로드 된 첨부파일의 삭제목록
|
||||
if (delFileIdx.value && delFileIdx.value.length > 0) {
|
||||
boardData.delFileIdx = [...delFileIdx.value];
|
||||
}
|
||||
|
||||
// 에디터에 업로드 된 이미지 인덱스 목록
|
||||
if (editorUploadedImgList.value && editorUploadedImgList.value.length > 0) {
|
||||
boardData.editorUploadedImgList = [...editorUploadedImgList.value];
|
||||
}
|
||||
|
||||
// 삭제할 에디터 이미지 인덱스
|
||||
if (editorDeleteImgList.value && editorDeleteImgList.value.length > 0) {
|
||||
boardData.editorDeleteImgList = [...editorDeleteImgList.value];
|
||||
}
|
||||
|
||||
const fileArray = newFileFilter(attachFiles);
|
||||
const formData = new FormData();
|
||||
|
||||
// formData에 boardData 추가
|
||||
Object.entries(boardData).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
|
||||
// formData에 새로 추가한 파일 추가
|
||||
fileArray.forEach((file, idx) => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
const { data } = await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
|
||||
if (data.code === 200) {
|
||||
toastStore.onToast('게시물이 수정되었습니다.', 's');
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
<tr>
|
||||
<th style="width: 11%" class="text-center fw-bold">번호</th>
|
||||
<th style="width: 45%" class="text-center fw-bold">제목</th>
|
||||
<th style="width: 10%" class="text-strat fw-bold">작성자</th>
|
||||
<th style="width: 10%" class="text-center fw-bold">작성자</th>
|
||||
<th style="width: 15%" class="text-center fw-bold">작성일</th>
|
||||
<th style="width: 9%" class="text-center fw-bold">조회수</th>
|
||||
</tr>
|
||||
@ -96,7 +96,7 @@
|
||||
<td class="cursor-pointer">
|
||||
<div class="d-flex flex-wrap align-items-center">
|
||||
{{ truncateTitle(post.title) }}
|
||||
<span v-if="post.commentCount" class="comment-count text-danger">[ {{ post.commentCount }} ]</span>
|
||||
<span v-if="post.commentCount" class="comment-count">[ {{ post.commentCount }} ]</span>
|
||||
<i v-if="post.img" class="bi bi-image mx-1"></i>
|
||||
<i
|
||||
v-if="Array.isArray(post.hasAttachment) && post.hasAttachment.length > 0"
|
||||
|
||||
@ -37,9 +37,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">
|
||||
카테고리를 선택해주세요.
|
||||
</div>
|
||||
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">카테고리를 선택해주세요.</div>
|
||||
</div>
|
||||
|
||||
<!-- 비밀번호 필드 (익명게시판 선택 시 활성화) -->
|
||||
@ -103,14 +101,11 @@
|
||||
@update:deleteImgIndexList="handleDeleteEditorImg"
|
||||
/>
|
||||
</div>
|
||||
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">
|
||||
내용을 입력해주세요.
|
||||
</div>
|
||||
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">내용을 입력해주세요.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 d-flex justify-content-end">
|
||||
<BackButton @click="goList" />
|
||||
<!-- 저장 버튼은 항상 활성화 -->
|
||||
<SaveButton @click="write" :isEnabled="isFileValid" />
|
||||
</div>
|
||||
</div>
|
||||
@ -120,7 +115,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, computed } from 'vue';
|
||||
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue';
|
||||
import QEditor from '@c/editor/QEditor.vue';
|
||||
import FormInput from '@c/input/FormInput.vue';
|
||||
import FormFile from '@c/input/FormFile.vue';
|
||||
@ -174,12 +169,10 @@
|
||||
|
||||
const fileCount = computed(() => attachFiles.value.length);
|
||||
|
||||
// 업데이트된 에디터 이미지 목록 업데이트
|
||||
const handleUpdateEditorImg = item => {
|
||||
editorUploadedImgList.value = item;
|
||||
};
|
||||
|
||||
// 삭제된 에디터 이미지 목록 업데이트
|
||||
const handleDeleteEditorImg = item => {
|
||||
editorDeleteImgList.value = item;
|
||||
};
|
||||
@ -194,8 +187,10 @@
|
||||
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
||||
return;
|
||||
}
|
||||
|
||||
fileError.value = '';
|
||||
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
||||
|
||||
autoIncrement.value++;
|
||||
};
|
||||
|
||||
@ -218,7 +213,7 @@
|
||||
const validateNickname = () => {
|
||||
if (categoryValue.value === 300102) {
|
||||
nickname.value = nickname.value.replace(/\s/g, ''); // 공백 제거
|
||||
nicknameAlert.value = nickname.value.length === 0;
|
||||
nicknameAlert.value = nickname.value.length === 0 ;
|
||||
} else {
|
||||
nicknameAlert.value = false;
|
||||
}
|
||||
@ -233,28 +228,19 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* validateContent:
|
||||
* - 내용이 없으면 contentAlert를 true로 설정
|
||||
* - 텍스트, 이미지, 비디오 중 하나라도 존재하면 유효한 콘텐츠로 판단
|
||||
*/
|
||||
const validateContent = () => {
|
||||
if (!content.value?.ops?.length) {
|
||||
contentAlert.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const hasText = content.value.ops.some(
|
||||
op => typeof op.insert === 'string' && op.insert.trim().length > 0
|
||||
);
|
||||
const hasImage = content.value.ops.some(
|
||||
op => op.insert && typeof op.insert === 'object' && op.insert.image
|
||||
);
|
||||
const hasVideo = content.value.ops.some(
|
||||
op => op.insert && typeof op.insert === 'object' && op.insert.video
|
||||
);
|
||||
// 이미지 포함 여부 확인
|
||||
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
|
||||
// 텍스트 포함 여부 확인
|
||||
const hasText = content.value.ops.some(op => typeof op.insert === 'string' && op.insert.trim().length > 0);
|
||||
|
||||
contentAlert.value = !(hasText || hasImage || hasVideo);
|
||||
// 텍스트 또는 이미지가 하나라도 있으면 유효한 내용
|
||||
contentAlert.value = !(hasText || hasImage);
|
||||
};
|
||||
|
||||
/** 글쓰기 */
|
||||
@ -278,7 +264,7 @@
|
||||
|
||||
try {
|
||||
const boardData = {
|
||||
LOCBRDTTL: title.value.trim(),
|
||||
LOCBRDTTL: title.value,
|
||||
LOCBRDCON: JSON.stringify(content.value), // Delta 포맷을 JSON으로 변환
|
||||
LOCBRDNIC: categoryValue.value === 300102 ? nickname.value : null,
|
||||
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
|
||||
@ -308,10 +294,10 @@
|
||||
formData.append('CMNFLEORG', fileNameWithoutExt);
|
||||
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
||||
formData.append('CMNFLESIZ', file.size);
|
||||
formData.append('file', file);
|
||||
formData.append('file', file); // 📌 실제 파일 추가
|
||||
|
||||
await axios.post(`board/${boardId}/attachments`, formData, { isFormData: true });
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -327,8 +313,8 @@
|
||||
router.push('/board');
|
||||
};
|
||||
|
||||
/** content 변경 감지 (deep 옵션 추가) */
|
||||
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
||||
watch(content, () => {
|
||||
validateContent();
|
||||
}, { deep: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
<div class="text-center">
|
||||
<label
|
||||
for="profilePic"
|
||||
class="rounded-circle cursor-pointer"
|
||||
class="rounded-circle m-auto ui-bg-cover position-relative cursor-pointer"
|
||||
id="profileLabel"
|
||||
:style="profilePreviewStyle"
|
||||
></label>
|
||||
<input type="file" id="profilePic" class="d-none object-fit-cover" @change="profileUpload" />
|
||||
<input type="file" id="profilePic" class="d-none" @change="profileUpload" />
|
||||
<span v-if="profilerr" class="invalid-feedback d-block">{{ profilerr }}</span>
|
||||
</div>
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
</span>
|
||||
|
||||
<!-- 기존 비밀번호 입력 -->
|
||||
<UserFormInput title="비밀번호 재설정" placeholder="기존 비밀번호를 입력하세요" name="currentPw" type="password"
|
||||
<UserFormInput title="기존 비밀번호" name="currentPw" type="password"
|
||||
:value="password.current" @update:data="password.current = $event"
|
||||
@blur="checkCurrentPassword" @keypress="noSpace" />
|
||||
<span v-if="passwordError" class="text-danger invalid-feedback mt-1 d-block">
|
||||
|
||||
@ -1,318 +0,0 @@
|
||||
<template>
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="card">
|
||||
<!-- 사원 목록이 없을 경우 표시 -->
|
||||
<div v-if="allUserList.length === 0" class="text-center my-4">
|
||||
<p class="text-muted">등록된 사원이 없습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- 사원 카드 리스트 영역 -->
|
||||
<div class="card-body">
|
||||
<div class="card-list">
|
||||
<div
|
||||
v-for="(person, index) in allUserList"
|
||||
:key="index"
|
||||
class="person-card"
|
||||
@click="openModal(person)"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
class="rounded-circle user-avatar pointer"
|
||||
:src="getProfileImage(person.MEMBERPRF)"
|
||||
:style="{ borderColor: person.usercolor }"
|
||||
@error="setDefaultImage"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3 class="person-name">{{ person.MEMBERNAM }}</h3>
|
||||
<p class="person-email">{{ person.MEMBERIDS }}@local-host.co.kr</p>
|
||||
<p class="person-phone">{{ person.MEMBERTEL }}</p>
|
||||
<small>
|
||||
{{ person.MEMBERARR }} {{ person.MEMBERDTL }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세보기 Modal -->
|
||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<button class="close-btn" @click="closeModal">×</button>
|
||||
<div class="modal-body">
|
||||
<img
|
||||
class="user-avatar2"
|
||||
:src="getProfileImage(selectedPerson.MEMBERPRF)"
|
||||
:style="{ borderColor: selectedPerson.usercolor }"
|
||||
@error="setDefaultImage"
|
||||
/>
|
||||
<h4>{{ selectedPerson.MEMBERNAM }}</h4>
|
||||
<p>{{ selectedPerson.MEMBERIDS }}@local-host.co.kr</p>
|
||||
<p>{{ selectedPerson.MEMBERTEL }}</p>
|
||||
<p>{{ selectedPerson.MEMBERARR }} {{ selectedPerson.MEMBERDTL }}</p>
|
||||
<hr />
|
||||
<!-- 추가 정보: 사용자가 속한 프로젝트 목록 -->
|
||||
<h5>참여 프로젝트</h5>
|
||||
<div v-if="memberProjects.length > 0" class="project-list-container">
|
||||
<ul>
|
||||
<li
|
||||
v-for="(project, idx) in memberProjects"
|
||||
:key="idx"
|
||||
class="project-item"
|
||||
>
|
||||
<span class="project-name">{{ project.PROJCTNAM }}</span>
|
||||
<span class="project-period">
|
||||
<!-- projectEndDate가 있는 경우 -->
|
||||
<!-- <template v-if="project.projectEndDate"> -->
|
||||
{{ project.userStartDate ? project.userStartDate : project.projectStartDate }} ~
|
||||
{{ project.userEndDate ? project.userEndDate : project.projectEndDate }}
|
||||
<!-- </template> -->
|
||||
<!-- 없으면 종료일 표시 안함 -->
|
||||
<!-- <template v-else>
|
||||
{{ project.userStartDate ? project.userStartDate : project.projectStartDate }} ~
|
||||
</template> -->
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>참여중인 프로젝트가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '@api' // API 호출용 Axios 인스턴스
|
||||
import { ref, onMounted } from 'vue'
|
||||
import SearchBar from '@c/search/SearchBar.vue'
|
||||
|
||||
export default {
|
||||
name: 'PeopleList',
|
||||
components: { SearchBar },
|
||||
setup() {
|
||||
const allUserList = ref([]) // 전체 사원 목록
|
||||
const user = ref({}) // 현재 로그인한 사용자 (필요 시 사용)
|
||||
const showModal = ref(false) // 모달 표시 여부
|
||||
const selectedPerson = ref({})// 모달에 표시할 선택된 사원 정보
|
||||
const memberProjects = ref([])// 선택된 사원의 프로젝트 목록
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await axios.get('user/allUserList')
|
||||
allUserList.value = response.data.data.allUserList
|
||||
user.value = response.data.data.user
|
||||
} catch (error) {
|
||||
console.error('사원 목록 조회 실패:', error)
|
||||
}
|
||||
})
|
||||
|
||||
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, '')
|
||||
const defaultProfile = '/img/icons/icon.png'
|
||||
|
||||
const getProfileImage = (profilePath) => {
|
||||
return profilePath && profilePath.trim()
|
||||
? `${baseUrl}upload/img/profile/${profilePath}`
|
||||
: defaultProfile
|
||||
}
|
||||
|
||||
const setDefaultImage = (event) => {
|
||||
event.target.src = defaultProfile
|
||||
}
|
||||
|
||||
// 참여 프로젝트 조회 API
|
||||
const fetchMemberProjects = async (memberSeq) => {
|
||||
try {
|
||||
const res = await axios.get(`project/people/${memberSeq}`)
|
||||
memberProjects.value = res.data.data
|
||||
} catch (error) {
|
||||
console.error('프로젝트 조회 실패:', error)
|
||||
memberProjects.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const openModal = (person) => {
|
||||
selectedPerson.value = person
|
||||
fetchMemberProjects(person.MEMBERSEQ)
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
allUserList,
|
||||
user,
|
||||
showModal,
|
||||
selectedPerson,
|
||||
memberProjects,
|
||||
openModal,
|
||||
closeModal,
|
||||
getProfileImage,
|
||||
defaultProfile,
|
||||
setDefaultImage
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-xxl {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.person-card {
|
||||
width: 280px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
transition: box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.person-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.person-card .card-header {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 160px;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ddd;
|
||||
display: block;
|
||||
margin: 1rem auto 0 auto;
|
||||
}
|
||||
|
||||
.user-avatar2 {
|
||||
width: 160px;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
margin: 1rem auto 0 auto;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.person-card .card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.person-name {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.person-email,
|
||||
.person-phone {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* 모달 스타일 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 111%;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
background: #fff;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
animation: slideDown 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-img {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 1rem;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 프로젝트 리스트 스타일 */
|
||||
.project-list-container {
|
||||
max-height: 200px; /* 필요에 따라 높이 조절 */
|
||||
overflow-y: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.project-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-period {
|
||||
font-size: 1rem;
|
||||
color: #888;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
0% {
|
||||
transform: translateY(-15%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -138,15 +138,13 @@ const checkedNames = (numList) => {
|
||||
}
|
||||
//투표종료
|
||||
const endVoteId = (endVoteId) => {
|
||||
if(confirm('투표를 종료하시겠습니까?')){
|
||||
$api.patch('vote/updateEndData',{
|
||||
endVoteId :endVoteId
|
||||
}).then((res)=>{
|
||||
if(res.data.status === 'OK'){
|
||||
getvoteList();
|
||||
}
|
||||
})
|
||||
}
|
||||
$api.patch('vote/updateEndData',{
|
||||
endVoteId :endVoteId
|
||||
}).then((res)=>{
|
||||
if(res.data.status === 'OK'){
|
||||
getvoteList();
|
||||
}
|
||||
})
|
||||
}
|
||||
//기한 지난 투표 종료
|
||||
const voteEnded = async (id) =>{
|
||||
@ -154,16 +152,14 @@ const voteEnded = async (id) =>{
|
||||
}
|
||||
//투표 삭제
|
||||
const voteDelete =(id) =>{
|
||||
if(confirm('투표를 삭제하시겠습니까?')){
|
||||
$api.patch('vote/updateDeleteData',{
|
||||
deleteVoteId :id
|
||||
}).then((res)=>{
|
||||
if(res.data.status === 'OK'){
|
||||
toastStore.onToast('투표가 삭제되었습니다.', 's');
|
||||
getvoteList();
|
||||
}
|
||||
})
|
||||
}
|
||||
$api.patch('vote/updateDeleteData',{
|
||||
deleteVoteId :id
|
||||
}).then((res)=>{
|
||||
if(res.data.status === 'OK'){
|
||||
toastStore.onToast('투표가 삭제되었습니다.', 's');
|
||||
getvoteList();
|
||||
}
|
||||
})
|
||||
}
|
||||
//랜덤 1위 뽑기
|
||||
const randomList = (data,id) =>{
|
||||
|
||||
@ -244,14 +244,4 @@ const goList = () => {
|
||||
.item-input {
|
||||
max-width: 200px;
|
||||
}
|
||||
.hidden-date-input {
|
||||
display: block; /* 한 줄 차지 */
|
||||
margin-top: 19.5px; /* form-input과 붙게 조정 */
|
||||
border: none;
|
||||
padding: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none; /* 사용자 클릭 못하게 */
|
||||
position: absolute; /* 시각적으로 띄워두기 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<div class="d-flex">
|
||||
<!-- 단어 갯수, 작성하기 -->
|
||||
<!-- 왼쪽 사이드바 -->
|
||||
<div v-if="cateList.length>0" class="sidebar position-sticky" style="top: 100px; max-width: 250px; min-width: 250px;">
|
||||
<div class="sidebar position-sticky" style="top: 100px; max-width: 250px; min-width: 250px;">
|
||||
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"
|
||||
:isActive="writeStore.activeItemId === 999999"/>
|
||||
<!-- ㄱ ㄴ ㄷ ㄹ -->
|
||||
@ -17,12 +17,8 @@
|
||||
<CategoryBtn :lists="cateList" @update:data="handleSelectedCategoryChange" :showAll="true" :selectedCategory="selectedCategory" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<WriteButton ref="writeButton" @click="writeStore.toggleItem(999999)" :isToggleEnabled="true"
|
||||
:isActive="writeStore.activeItemId === 999999"/>
|
||||
</div>
|
||||
|
||||
<!-- 용어 리스트 컨텐츠 -->
|
||||
<!-- 용어 리스트 컨텐츠 -->
|
||||
<div class="flex-grow-1">
|
||||
<!-- 작성 -->
|
||||
<div v-if="writeStore.isItemActive(999999)" class="ms-3 card p-5 mb-2">
|
||||
@ -33,9 +29,9 @@
|
||||
<!-- 에러 메시지 -->
|
||||
<div v-if="error" class="fw-bold text-danger">{{ error }}</div>
|
||||
<!-- 단어 목록 -->
|
||||
<ul v-if="total > 0" class="ms-3 list-unstyled">
|
||||
<ul v-if="total > 0" class="ms-3 list-unstyled" style="overflow-x: hidden; word-wrap: break-word;">
|
||||
<DictCard
|
||||
class="q-editor-container"
|
||||
class="DictCard q-editor-container"
|
||||
v-for="item in wordList"
|
||||
:key="item.WRDDICSEQ"
|
||||
:item="item"
|
||||
@ -335,12 +331,15 @@ import { useRoute } from 'vue-router';
|
||||
top: 5px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.DictCard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.q-editor-container {
|
||||
max-width: 100%; /* 영역이 넘치지 않게 */
|
||||
overflow: auto; /* 넘치는 내용은 스크롤로 처리 */
|
||||
word-wrap: break-word; /* 긴 단어는 자동으로 줄바꿈 */
|
||||
white-space: normal; /* 내용이 길어지면 자동으로 줄바꿈 */
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user