Compare commits
35 Commits
250408_par
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fb90c7330 | |||
| fd1c8c4053 | |||
| 90ed8819ad | |||
| 130c8fced0 | |||
| 3804abfa09 | |||
| 1be47c1a58 | |||
| cb5e274ac1 | |||
| 3a0b09624b | |||
| 9dfe130500 | |||
| 96411af84a | |||
| e12e9b8bc8 | |||
| db06418389 | |||
| 549a01d454 | |||
| 3cdba34130 | |||
| d3ba7d446e | |||
| cca27b9583 | |||
| 3d147076ef | |||
| e75ca56f7d | |||
| 5be05bbab6 | |||
| 93b8843dd7 | |||
| 2bd64142ac | |||
| 4c5b4481b6 | |||
| 79ce960a3a | |||
| 888a733f4b | |||
| 8361a02dc8 | |||
| 11ebea8ccd | |||
| 103f5f3a62 | |||
| 14c8fb4108 | |||
| 803e6da4b3 | |||
| ba9a752250 | |||
| 5c7f7c6346 | |||
| 5b24a0254b | |||
| 52c3bbdf6c | |||
| a27de5443a | |||
| 1b354d464c |
@ -55,8 +55,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Quill from 'quill';
|
import Quill from 'quill';
|
||||||
import 'quill/dist/quill.snow.css';
|
import 'quill/dist/quill.snow.css';
|
||||||
import { onMounted, ref, watch, defineEmits, defineProps } from 'vue';
|
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
import { onMounted, ref, watch, defineEmits, defineProps } from 'vue';
|
||||||
|
import { useToastStore } from '@s/toastStore';
|
||||||
|
|
||||||
|
const toastStore = useToastStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isAlert: {
|
isAlert: {
|
||||||
@ -132,15 +135,36 @@
|
|||||||
initCheckImageIndex();
|
initCheckImageIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 영상 넣기.
|
// 영상 넣기
|
||||||
quillInstance.getModule('toolbar').addHandler('video', () => {
|
quillInstance.getModule('toolbar').addHandler('video', () => {
|
||||||
const url = prompt('영상 URL을 입력하세요:');
|
const url = prompt('YouTube 영상 URL을 입력하세요:');
|
||||||
if (url) {
|
let src = '';
|
||||||
// 에디터에 iframe 형태로 영상 삽입
|
if (!url || url.trim() == '') return;
|
||||||
const index = quillInstance.getSelection().index; // 현재 커서 위치
|
|
||||||
quillInstance.insertEmbed(index, 'video', url);
|
// 일반 youtube url
|
||||||
quillInstance.setSelection(index + 1); // 커서를 삽입된 콘텐츠 뒤로 이동
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 이미지 업로드 기능 처리
|
// 이미지 업로드 기능 처리
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
:value="computedValue"
|
:value="computedValue"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:maxLength="maxlength"
|
:maxLength="maxlength"
|
||||||
:placeholder="title"
|
:placeholder="placeholder ? placeholder : title"
|
||||||
@blur="$emit('blur')"
|
@blur="$emit('blur')"
|
||||||
/>
|
/>
|
||||||
<span class="input-group-text">@ localhost.co.kr</span>
|
<span class="input-group-text">@ localhost.co.kr</span>
|
||||||
@ -29,7 +29,7 @@
|
|||||||
:value="computedValue"
|
:value="computedValue"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:maxLength="maxlength"
|
:maxLength="maxlength"
|
||||||
:placeholder="title"
|
:placeholder="placeholder ? placeholder : title"
|
||||||
@blur="$emit('blur')"
|
@blur="$emit('blur')"
|
||||||
@click="handleDateClick"
|
@click="handleDateClick"
|
||||||
ref="inputElement"
|
ref="inputElement"
|
||||||
@ -89,6 +89,10 @@
|
|||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['update:data', 'update:alert', 'blur']);
|
const emits = defineEmits(['update:data', 'update:alert', 'blur']);
|
||||||
|
|||||||
@ -14,7 +14,14 @@
|
|||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<CommuterBtn :userId="user.id" :checkedInProject="checkedInProject || {}" ref="workTimeComponentRef" />
|
<CommuterBtn
|
||||||
|
ref="workTimeComponentRef"
|
||||||
|
:userId="user.id"
|
||||||
|
:checkedInProject="checkedInProject || {}"
|
||||||
|
:pendingProjectChange="pendingProjectChange"
|
||||||
|
@update:pendingProjectChange="pendingProjectChange = $event"
|
||||||
|
@leaveTimeUpdated="handleLeaveTimeUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
<MainEventList
|
<MainEventList
|
||||||
:categoryList="categoryList"
|
:categoryList="categoryList"
|
||||||
@ -99,6 +106,7 @@
|
|||||||
|
|
||||||
const selectedProject = ref(null);
|
const selectedProject = ref(null);
|
||||||
const checkedInProject = ref(null);
|
const checkedInProject = ref(null);
|
||||||
|
const pendingProjectChange = ref(null);
|
||||||
|
|
||||||
// 이벤트 모달 관련
|
// 이벤트 모달 관련
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
@ -605,10 +613,48 @@
|
|||||||
if (newProject) {
|
if (newProject) {
|
||||||
selectedProject.value = newProject.PROJCTSEQ;
|
selectedProject.value = newProject.PROJCTSEQ;
|
||||||
checkedInProject.value = newProject;
|
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 () => {
|
onMounted(async () => {
|
||||||
await userStore.userInfo();
|
await userStore.userInfo();
|
||||||
user.value = userStore.user;
|
user.value = userStore.user;
|
||||||
|
|||||||
@ -32,12 +32,10 @@
|
|||||||
@error="setDefaultImage"
|
@error="setDefaultImage"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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-event ps-1" style="cursor: pointer;" @click.stop="openModal(item.localVote.LOCVOTSEQ)" >
|
||||||
<div class="timeline-header ">
|
<div class="timeline-header ">
|
||||||
<small ><strong>{{ truncateTitle(item.localVote.LOCVOTTTL) }}</strong></small>
|
<small ><strong>{{ truncateTitle(item.localVote.LOCVOTTTL) }}</strong></small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small class="d-flex align-items-center lh-1 me-4 mb-4 mb-sm-0"
|
<small class="d-flex align-items-center lh-1 me-4 mb-4 mb-sm-0"
|
||||||
:style="{ color: getDaysAgo(item.localVote.formatted_LOCVOTEDT) == '금일 종료' ? 'red' : '' }">
|
:style="{ color: getDaysAgo(item.localVote.formatted_LOCVOTEDT) == '금일 종료' ? 'red' : '' }">
|
||||||
⏰{{getDaysAgo(item.localVote.formatted_LOCVOTEDT)}}({{item.localVote.total_voted}}/{{ item.localVote.total_votable }})
|
⏰{{getDaysAgo(item.localVote.formatted_LOCVOTEDT)}}({{item.localVote.total_voted}}/{{ item.localVote.total_votable }})
|
||||||
@ -53,7 +51,6 @@
|
|||||||
<div class="card-body" v-else>
|
<div class="card-body" v-else>
|
||||||
진행중인 투표가 없습니다.
|
진행중인 투표가 없습니다.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--투표 모달 -->
|
<!--투표 모달 -->
|
||||||
@ -211,7 +208,7 @@ const voteDelete =(id) =>{
|
|||||||
}
|
}
|
||||||
// 제목이 14글자 넘어가면 ... 처리하는 함수
|
// 제목이 14글자 넘어가면 ... 처리하는 함수
|
||||||
const truncateTitle = title => {
|
const truncateTitle = title => {
|
||||||
return title.length > 10 ? title.slice(0, 10) + '...' : title;
|
return title.length > 10 ? title.slice(0, 10) + '...' : title;
|
||||||
};
|
};
|
||||||
|
|
||||||
//투표이동
|
//투표이동
|
||||||
@ -229,13 +226,11 @@ const goVoteList = () =>{
|
|||||||
const getDaysAgo = (dateString) => {
|
const getDaysAgo = (dateString) => {
|
||||||
const inputDate = new Date(dateString); // 문자열을 Date 객체로 변환
|
const inputDate = new Date(dateString); // 문자열을 Date 객체로 변환
|
||||||
const today = new Date(); // 현재 날짜 가져오기
|
const today = new Date(); // 현재 날짜 가져오기
|
||||||
|
|
||||||
const input = new Date(inputDate.getFullYear(), inputDate.getMonth(), inputDate.getDate());
|
const input = new Date(inputDate.getFullYear(), inputDate.getMonth(), inputDate.getDate());
|
||||||
const now = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
const now = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||||
|
|
||||||
const timeDiff = now - input;
|
const timeDiff = now - input;
|
||||||
const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
|
const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
|
||||||
// 오늘 날짜인 경우 "오늘" 반환
|
// 오늘 날짜인 경우
|
||||||
if (dayDiff === 0) return "금일 종료";
|
if (dayDiff === 0) return "금일 종료";
|
||||||
|
|
||||||
return `종료 ${Math.abs(dayDiff)}일 전`;
|
return `종료 ${Math.abs(dayDiff)}일 전`;
|
||||||
@ -245,6 +240,5 @@ const getDaysAgo = (dateString) => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
border: 3px solid;
|
border: 3px solid;
|
||||||
padding: 0.1px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -135,6 +135,5 @@ return title.length > 25 ? title.slice(0, 25) + '...' : title;
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
border: 3px solid;
|
border: 3px solid;
|
||||||
padding: 0.1px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -50,14 +50,15 @@
|
|||||||
<label class="switch"
|
<label class="switch"
|
||||||
><input
|
><input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="checked"
|
:checked="member.checked"
|
||||||
@change="handleRegisterMember(member.MEMBERSEQ)" /><span class="slider round"></span
|
@click="handleRegisterMember($event, member)" />
|
||||||
|
<span class="slider round"></span
|
||||||
></label>
|
></label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn-close btn-close-sm"
|
class="btn-close btn-close-sm"
|
||||||
style="position: absolute; top: 10px; right: 10px"
|
style="position: absolute; top: 10px; right: 10px"
|
||||||
@click="handleRejectMember(member.MEMBERSEQ)"
|
@click="handleRejectMember(member)"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -76,30 +77,37 @@
|
|||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
|
||||||
const memberList = ref([]);
|
const memberList = ref([]);
|
||||||
const checked = ref(false);
|
|
||||||
const toast = useToastStore();
|
const toast = useToastStore();
|
||||||
const imgURL = import.meta.env.VITE_SERVER_IMG_URL;
|
const imgURL = import.meta.env.VITE_SERVER_IMG_URL;
|
||||||
|
|
||||||
// 조회 api
|
// 조회 api
|
||||||
const fetchRegisterMemberList = async () => {
|
const fetchRegisterMemberList = async () => {
|
||||||
const { data } = await $api.get('main/registerMemberList');
|
const { data } = await $api.get('main/registerMemberList');
|
||||||
if (data?.data) memberList.value = data.data;
|
if (data?.data) {
|
||||||
|
memberList.value = data.data.map(member => ({
|
||||||
|
...member,
|
||||||
|
checked: false, // 각 항목에 checked 값 설정
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 사원 등록 api
|
// 사원 등록 api
|
||||||
const handleRegisterMember = async memberSeq => {
|
const handleRegisterMember = async (e, member) => {
|
||||||
const { data } = await $api.post('main/registerMember', { memberSeq: memberSeq });
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { data } = await $api.post('main/registerMember', { memberSeq: member.MEMBERSEQ });
|
||||||
if (data?.data) {
|
if (data?.data) {
|
||||||
|
member.checked = true;
|
||||||
toast.onToast(data.data, 's');
|
toast.onToast(data.data, 's');
|
||||||
fetchRegisterMemberList();
|
fetchRegisterMemberList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 사원 등록 미승인 api
|
// 사원 등록 미승인 api
|
||||||
const handleRejectMember = async memberSeq => {
|
const handleRejectMember = async member => {
|
||||||
if (!confirm('해당 사원 등록을 거절하시겠습니까?')) return;
|
if (!confirm('해당 사원 등록을 거절하시겠습니까?')) return;
|
||||||
|
|
||||||
const { data } = await $api.post('main/rejectMember', { memberSeq: memberSeq });
|
const { data } = await $api.post('main/rejectMember', { memberSeq: member.MEMBERSEQ });
|
||||||
if (data?.data) {
|
if (data?.data) {
|
||||||
toast.onToast(data.data, 's');
|
toast.onToast(data.data, 's');
|
||||||
fetchRegisterMemberList();
|
fetchRegisterMemberList();
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
@update:alert="idAlert = $event"
|
@update:alert="idAlert = $event"
|
||||||
@blur="checkIdDuplicate"
|
@blur="checkIdDuplicate"
|
||||||
:value="id"
|
:value="id"
|
||||||
|
@keypress="noSpace"
|
||||||
/>
|
/>
|
||||||
<span v-if="idError" class="invalid-feedback d-block">{{ idError }}</span>
|
<span v-if="idError" class="invalid-feedback d-block">{{ idError }}</span>
|
||||||
|
|
||||||
@ -37,6 +38,7 @@
|
|||||||
@update:data="password = $event"
|
@update:data="password = $event"
|
||||||
@update:alert="passwordAlert = $event"
|
@update:alert="passwordAlert = $event"
|
||||||
:value="password"
|
:value="password"
|
||||||
|
@keypress="noSpace"
|
||||||
/>
|
/>
|
||||||
<span v-if="passwordError" class="invalid-feedback d-block">{{ passwordError }}</span>
|
<span v-if="passwordError" class="invalid-feedback d-block">{{ passwordError }}</span>
|
||||||
|
|
||||||
@ -49,6 +51,7 @@
|
|||||||
@update:data="passwordcheck = $event"
|
@update:data="passwordcheck = $event"
|
||||||
@update:alert="passwordcheckAlert = $event"
|
@update:alert="passwordcheckAlert = $event"
|
||||||
:value="passwordcheck"
|
:value="passwordcheck"
|
||||||
|
@keypress="noSpace"
|
||||||
/>
|
/>
|
||||||
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
|
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
|
||||||
|
|
||||||
@ -82,6 +85,7 @@
|
|||||||
@update:data="name = $event"
|
@update:data="name = $event"
|
||||||
@update:alert="nameAlert = $event"
|
@update:alert="nameAlert = $event"
|
||||||
:value="name"
|
:value="name"
|
||||||
|
@keypress="noSpace"
|
||||||
class="me-2 w-50"
|
class="me-2 w-50"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -214,6 +218,10 @@
|
|||||||
|
|
||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
|
|
||||||
|
const noSpace = (e) => {
|
||||||
|
if (e.key === ' ') e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
// 프로필 체크
|
// 프로필 체크
|
||||||
const profileValid = (size, type) => {
|
const profileValid = (size, type) => {
|
||||||
const maxSize = 5 * 1024 * 1024;
|
const maxSize = 5 * 1024 * 1024;
|
||||||
@ -344,6 +352,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(password, (newValue) => {
|
watch(password, (newValue) => {
|
||||||
|
|
||||||
if (newValue && newValue.length >= 4) {
|
if (newValue && newValue.length >= 4) {
|
||||||
passwordErrorAlert.value = false;
|
passwordErrorAlert.value = false;
|
||||||
passwordError.value = '';
|
passwordError.value = '';
|
||||||
@ -396,8 +405,10 @@
|
|||||||
} else {
|
} else {
|
||||||
passwordError.value = '';
|
passwordError.value = '';
|
||||||
}
|
}
|
||||||
|
const phoneRegex = /^010\d{8}$/;
|
||||||
|
const isFormatValid = phoneRegex.test(phone.value);
|
||||||
|
|
||||||
if (!/^\d+$/.test(phone.value)) {
|
if (!/^\d+$/.test(phone.value) || !isFormatValid) {
|
||||||
phoneAlert.value = true;
|
phoneAlert.value = true;
|
||||||
} else {
|
} else {
|
||||||
phoneAlert.value = false;
|
phoneAlert.value = false;
|
||||||
@ -434,13 +445,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('memberIds', id.value);
|
formData.append('memberIds', id.value.trim());
|
||||||
formData.append('memberPwd', password.value);
|
formData.append('memberPwd', password.value.trim());
|
||||||
formData.append('memberPwh', pwhint.value);
|
formData.append('memberPwh', pwhint.value);
|
||||||
formData.append('memberPwr', pwhintRes.value);
|
formData.append('memberPwr', pwhintRes.value.trim());
|
||||||
formData.append('memberNam', name.value);
|
formData.append('memberNam', name.value.trim());
|
||||||
formData.append('memberArr', address.value);
|
formData.append('memberArr', address.value);
|
||||||
formData.append('memberDtl', detailAddress.value);
|
formData.append('memberDtl', detailAddress.value.trim());
|
||||||
formData.append('memberZip', postcode.value);
|
formData.append('memberZip', postcode.value);
|
||||||
formData.append('memberBth', birth.value);
|
formData.append('memberBth', birth.value);
|
||||||
formData.append('memberTel', phone.value);
|
formData.append('memberTel', phone.value);
|
||||||
|
|||||||
@ -164,7 +164,6 @@ const toggleEdit = async () => {
|
|||||||
|
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
border: 3px solid;
|
border: 3px solid;
|
||||||
padding: 0.1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-btn {
|
.edit-btn {
|
||||||
|
|||||||
@ -80,12 +80,12 @@
|
|||||||
<div class="text-truncate">Authorization</div>
|
<div class="text-truncate">Authorization</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
<!-- <li class="menu-item" :class="$route.path.includes('/sample') ? 'active' : ''">
|
<li class="menu-item" :class="$route.path.includes('/people') ? 'active' : ''">
|
||||||
<RouterLink class="menu-link" to="/sample"> <i class="bi "></i>
|
<RouterLink class="menu-link" to="/people"> <i class="bi "></i>
|
||||||
<i class="menu-icon tf-icons bx bx-calendar"></i>
|
<i class="menu-icon icon-base bi bi-people-fill"></i>
|
||||||
<div class="text-truncate">Sample</div>
|
<div class="text-truncate">people</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li> -->
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
<!-- / Menu -->
|
<!-- / Menu -->
|
||||||
@ -94,6 +94,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useUserInfoStore } from '@s/useUserInfoStore';
|
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||||
|
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||||
|
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
const allowedUserId = 1; // 특정 ID (변경필요!!)
|
const allowedUserId = 1; // 특정 ID (변경필요!!)
|
||||||
|
|||||||
@ -38,25 +38,36 @@
|
|||||||
<!-- Notification -->
|
<!-- Notification -->
|
||||||
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2 p-0">
|
<li class="nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2 p-0">
|
||||||
<a
|
<a
|
||||||
class="nav-link dropdown-toggle hide-arrow p-0"
|
class="nav-link dropdown-toggle hide-arrow p-0"
|
||||||
href="javascript:void(0);"
|
href="javascript:void(0);"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
data-bs-auto-close="outside"
|
data-bs-auto-close="outside"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
<span class="position-relative">
|
<span class="position-relative">
|
||||||
<i class="bx bx-bell bx-md"></i>
|
<i class="bx bx-bell bx-md"></i>
|
||||||
<span class="badge rounded-pill bg-danger badge-dot badge-notifications border"></span>
|
<!-- 알림이 있을 경우에만 뱃지를 표시 -->
|
||||||
</span>
|
<span
|
||||||
|
v-if="notificationCount > 0"
|
||||||
|
class="badge rounded-pill bg-danger badge-dot badge-notifications border"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end p-0">
|
<ul class="dropdown-menu dropdown-menu-end p-0">
|
||||||
<li class="dropdown-notifications-list scrollable-container p-3">
|
<li class="dropdown-notifications-list scrollable-container p-3">
|
||||||
알림이 없습니다.
|
<!-- 알림이 없으면 "알림이 없습니다." 메시지 표시 -->
|
||||||
<!-- <ul class="list-group list-group-flush">
|
<div v-if="notificationCount === 0">
|
||||||
<li class="list-group-item list-group-item-action dropdown-notifications-item">
|
알림이 없습니다.
|
||||||
</li>
|
</div>
|
||||||
</ul> -->
|
<!-- 알림이 있을 때 목록 렌더링-->
|
||||||
</li>
|
<div v-else>
|
||||||
|
<ul>
|
||||||
|
<li v-for="notification in notifications" :key="notification.id">
|
||||||
|
{{ notification.text }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<!--/ Notification -->
|
<!--/ Notification -->
|
||||||
@ -120,6 +131,8 @@
|
|||||||
const selectedProject = ref(null);
|
const selectedProject = ref(null);
|
||||||
const weather = ref({});
|
const weather = ref({});
|
||||||
const dailyWeatherList = ref([]);
|
const dailyWeatherList = ref([]);
|
||||||
|
const notifications = ref([]);
|
||||||
|
const notificationCount = ref(0);
|
||||||
|
|
||||||
const weatherReady = computed(() => {
|
const weatherReady = computed(() => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -109,6 +109,12 @@ const routes = [
|
|||||||
component: () => import('@v/admin/TheAuthorization.vue'),
|
component: () => import('@v/admin/TheAuthorization.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/people',
|
||||||
|
name: 'people',
|
||||||
|
component: () => import('@v/people/PeopleList.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/error/400',
|
path: '/error/400',
|
||||||
name: 'Error400',
|
name: 'Error400',
|
||||||
@ -148,7 +154,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
|
|
||||||
// Authorization 페이지는 ID가 26이 아니면 접근 차단
|
// Authorization 페이지는 ID가 26이 아니면 접근 차단
|
||||||
if (to.path === '/authorization' && userId !== allowedUserId) {
|
if (to.path === '/authorization' && userId !== allowedUserId) {
|
||||||
return next('/');
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동
|
// 비로그인 사용자만 접근 가능한 페이지인데 로그인된 경우 → 홈으로 이동
|
||||||
|
|||||||
@ -40,7 +40,13 @@ export const useWeatherStore = defineStore('weather', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dailyWeatherList.value = resData.dailyWeatherList;
|
// 검은색 태양 아이콘 변경
|
||||||
|
dailyWeatherList.value = resData.dailyWeatherList.map(w => {
|
||||||
|
return {
|
||||||
|
...w,
|
||||||
|
icon: w.icon.replace(/n$/, 'd'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const todayStr = now.toISOString().split('T')[0];
|
const todayStr = now.toISOString().split('T')[0];
|
||||||
|
|||||||
@ -21,18 +21,18 @@
|
|||||||
import MainVote from '@c/main/MainVote.vue';
|
import MainVote from '@c/main/MainVote.vue';
|
||||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
import $api from '@api';
|
||||||
|
|
||||||
const userStore = useUserInfoStore();
|
const userStore = useUserInfoStore();
|
||||||
const user = ref();
|
|
||||||
const isAdmin = ref(false);
|
const isAdmin = ref(false);
|
||||||
|
|
||||||
const checkAdmin = user => {
|
const checkAdmin = async user => {
|
||||||
return user?.value?.role === 'ROLE_ADMIN' ? true : false;
|
const { data } = await $api.post('user/authCheck', { memberId: user.loginId });
|
||||||
|
return data.data === 'ROLE_ADMIN' ? true : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await userStore.userInfo();
|
await userStore.userInfo();
|
||||||
user.value = userStore.user;
|
isAdmin.value = await checkAdmin(userStore.user);
|
||||||
isAdmin.value = await checkAdmin(user);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,26 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container text-center flex-grow-1 container-p-y">
|
<div class="container text-center flex-grow-1 container-p-y">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex flex-column">
|
<div class="card-header d-flex flex-column">
|
||||||
<h3>관리자 권한 부여</h3>
|
<h3>관리자 권한 부여</h3>
|
||||||
<div class="user-card-container">
|
<div class="user-card-container">
|
||||||
<div v-for="user in users" :key="user.id" class="user-card">
|
<div v-for="user in users" :key="user.id" class="user-card">
|
||||||
<!-- 프로필 사진 -->
|
<!-- 프로필 사진 -->
|
||||||
<img :src="getProfileImage(user.photo)" class="profile-img" alt="프로필 사진" @error="setDefaultImage" />
|
<img
|
||||||
<!-- 사용자 정보 -->
|
:src="getProfileImage(user.photo)"
|
||||||
<div class="user-info">
|
class="user-avatar2"
|
||||||
<h5>{{ user.name }}</h5>
|
alt="프로필 사진"
|
||||||
</div>
|
@error="setDefaultImage"
|
||||||
<!-- 권한 토글 버튼 -->
|
/>
|
||||||
<label class="switch me-0">
|
<!-- 사용자 정보 -->
|
||||||
<input type="checkbox" :checked="user.isAdmin" @change="toggleAdmin(user)" />
|
<div class="user-info">
|
||||||
<span class="slider round"></span>
|
<h5>{{ user.name }}</h5>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -32,65 +41,80 @@ const users = ref([]);
|
|||||||
const toastStore = useToastStore();
|
const toastStore = useToastStore();
|
||||||
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, "");
|
const baseUrl = axios.defaults.baseURL.replace(/api\/$/, "");
|
||||||
const defaultProfile = "/img/icons/icon.png";
|
const defaultProfile = "/img/icons/icon.png";
|
||||||
const allowedUserId = 1; // 특정 ID (변경필요!!)
|
const allowedUserId = 1; // 특정 ID (필요에 따라 변경)
|
||||||
|
|
||||||
// 사용자 목록 가져오기
|
// 사용자 목록 가져오기
|
||||||
async function fetchUsers() {
|
async function fetchUsers() {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('admin/users'); // API 경로 확인 필요
|
const response = await axios.get('admin/users'); // API 경로 확인
|
||||||
|
if (!response.data || !Array.isArray(response.data.data)) {
|
||||||
// API 응답 구조 확인 후 데이터가 배열인지 체크
|
throw new Error("올바른 데이터 형식이 아닙니다.");
|
||||||
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) {
|
function getProfileImage(photo) {
|
||||||
return photo || defaultProfile;
|
return photo || defaultProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이미지 로드 오류 시 기본 이미지 설정
|
// 이미지 로드 오류 시 기본 이미지 설정
|
||||||
function setDefaultImage(event) {
|
function setDefaultImage(event) {
|
||||||
event.target.src = defaultProfile;
|
event.target.src = defaultProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 관리자 권한 토글 함수
|
// 권한 토글 시 기본 동작 막고 직접 제어하는 함수
|
||||||
async function toggleAdmin(user) {
|
async function handleToggle(event, user) {
|
||||||
const requestData = {
|
// Prevent the default checkbox toggle behavior
|
||||||
id: user.id,
|
event.preventDefault();
|
||||||
role: user.isAdmin ? 'MEMBER' : 'ADMIN'
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const response = await axios.put('admin/role', requestData);
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
// 저장: 현재 상태를 기반으로 변경 요청 (체크박스는 아직 변하지 않았음)
|
||||||
user.isAdmin = !user.isAdmin;
|
const originalState = user.isAdmin;
|
||||||
toastStore.onToast(`'${user.name}'의 권한이 '${requestData.role}'(으)로 변경되었습니다.`, 's');
|
const newState = !originalState;
|
||||||
} else {
|
|
||||||
throw new Error('권한 변경 실패');
|
const requestData = {
|
||||||
}
|
id: user.id,
|
||||||
} catch (error) {
|
role: originalState ? 'MEMBER' : 'ADMIN'
|
||||||
toastStore.onToast('권한 변경에 실패했습니다.', 'e');
|
};
|
||||||
|
|
||||||
|
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('권한 변경 실패');
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 에러 발생 시 상태를 변경하지 않음
|
||||||
|
toastStore.onToast('권한 변경에 실패했습니다.', 'e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(fetchUsers);
|
onMounted(fetchUsers);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
|
|||||||
@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
// 상태 변수
|
// 상태 변수
|
||||||
const title = ref('');
|
const title = ref('');
|
||||||
const content = ref('');
|
const content = ref({ ops: [] });
|
||||||
const autoIncrement = ref(0);
|
const autoIncrement = ref(0);
|
||||||
|
|
||||||
// 경고 상태
|
// 경고 상태
|
||||||
@ -130,10 +130,9 @@
|
|||||||
// 최초 업데이트 감지 여부
|
// 최초 업데이트 감지 여부
|
||||||
const isFirstContentUpdate = ref(true);
|
const isFirstContentUpdate = ref(true);
|
||||||
|
|
||||||
// 에디터에서 데이터 업데이트 시
|
// 에디터 데이터 업데이트 시 처리 (최초 데이터 저장)
|
||||||
const handleEditorDataUpdate = data => {
|
const handleEditorDataUpdate = data => {
|
||||||
content.value = data;
|
content.value = data;
|
||||||
|
|
||||||
if (isFirstContentUpdate.value) {
|
if (isFirstContentUpdate.value) {
|
||||||
originalContent.value = structuredClone(data);
|
originalContent.value = structuredClone(data);
|
||||||
isFirstContentUpdate.value = false;
|
isFirstContentUpdate.value = false;
|
||||||
@ -141,23 +140,28 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// isDeltaChanged 함수 수정 (내장 diff 대신 텍스트, 이미지, 비디오 각각을 직접 비교)
|
||||||
function isDeltaChanged(current, original) {
|
function isDeltaChanged(current, original) {
|
||||||
const Delta = Quill.import('delta');
|
const Delta = Quill.import('delta');
|
||||||
const currentDelta = new Delta(current || []);
|
const currentDelta = new Delta(current || []);
|
||||||
const originalDelta = new Delta(original || []);
|
const originalDelta = new Delta(original || []);
|
||||||
|
|
||||||
const diff = originalDelta.diff(currentDelta);
|
// 텍스트 추출
|
||||||
if (!diff || diff.ops.length === 0) return false;
|
|
||||||
|
|
||||||
// 텍스트만 비교해서 완전 동일한지 확인
|
|
||||||
const getPlainText = delta =>
|
const getPlainText = delta =>
|
||||||
(delta.ops || [])
|
(delta.ops || [])
|
||||||
.filter(op => typeof op.insert === 'string')
|
.filter(op => typeof op.insert === 'string')
|
||||||
.map(op => op.insert)
|
.map(op => op.insert)
|
||||||
.join('');
|
.join('');
|
||||||
|
// 이미지 URL 추출
|
||||||
const getImages = delta =>
|
const getImages = delta =>
|
||||||
(delta.ops || []).filter(op => typeof op.insert === 'object' && op.insert.image).map(op => op.insert.image);
|
(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);
|
||||||
|
|
||||||
const textCurrent = getPlainText(currentDelta);
|
const textCurrent = getPlainText(currentDelta);
|
||||||
const textOriginal = getPlainText(originalDelta);
|
const textOriginal = getPlainText(originalDelta);
|
||||||
@ -165,22 +169,27 @@
|
|||||||
const imgsCurrent = getImages(currentDelta);
|
const imgsCurrent = getImages(currentDelta);
|
||||||
const imgsOriginal = getImages(originalDelta);
|
const imgsOriginal = getImages(originalDelta);
|
||||||
|
|
||||||
const textEqual = textCurrent === textOriginal;
|
const vidsCurrent = getVideos(currentDelta);
|
||||||
const imageEqual = JSON.stringify(imgsCurrent) === JSON.stringify(imgsOriginal);
|
const vidsOriginal = getVideos(originalDelta);
|
||||||
|
|
||||||
return !(textEqual && imageEqual); // 둘 다 같아야 false
|
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]);
|
||||||
|
|
||||||
|
return !(textEqual && imageEqual && videoEqual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 게시물 변경 여부 계산
|
||||||
const isChanged = computed(() => {
|
const isChanged = computed(() => {
|
||||||
if (!contentInitialized.value) return false;
|
if (!contentInitialized.value) return false;
|
||||||
const isTitleChanged = title.value !== originalTitle.value;
|
const isTitleChanged = title.value !== originalTitle.value;
|
||||||
const isContentChanged = isDeltaChanged(content.value, originalContent.value);
|
const isContentChanged = isDeltaChanged(content.value, originalContent.value);
|
||||||
const isFilesChanged =
|
const isFilesChanged =
|
||||||
attachFiles.value.some(f => !f.id) || // id 없는 새 파일이 있는 경우
|
attachFiles.value.some(f => !f.id) || // 신규 파일 존재
|
||||||
delFileIdx.value.length > 0 || // 삭제된 파일이 있는 경우
|
delFileIdx.value.length > 0 || // 삭제된 파일이 있는 경우
|
||||||
!isSameFiles(
|
!isSameFiles(
|
||||||
attachFiles.value.filter(f => f.id), // 기존 파일(id 있는 것만)
|
attachFiles.value.filter(f => f.id), // 기존 파일만 비교
|
||||||
originalFiles.value,
|
originalFiles.value
|
||||||
);
|
);
|
||||||
return isTitleChanged || isContentChanged || isFilesChanged;
|
return isTitleChanged || isContentChanged || isFilesChanged;
|
||||||
});
|
});
|
||||||
@ -188,10 +197,8 @@
|
|||||||
// 파일 비교 함수
|
// 파일 비교 함수
|
||||||
function isSameFiles(current, original) {
|
function isSameFiles(current, original) {
|
||||||
if (current.length !== original.length) return false;
|
if (current.length !== original.length) return false;
|
||||||
|
|
||||||
const sortedCurrent = [...current].sort((a, b) => a.id - b.id);
|
const sortedCurrent = [...current].sort((a, b) => a.id - b.id);
|
||||||
const sortedOriginal = [...original].sort((a, b) => a.id - b.id);
|
const sortedOriginal = [...original].sort((a, b) => a.id - b.id);
|
||||||
|
|
||||||
return sortedCurrent.every((file, idx) => {
|
return sortedCurrent.every((file, idx) => {
|
||||||
return file.id === sortedOriginal[idx].id && file.name === sortedOriginal[idx].name;
|
return file.id === sortedOriginal[idx].id && file.name === sortedOriginal[idx].name;
|
||||||
});
|
});
|
||||||
@ -199,31 +206,24 @@
|
|||||||
|
|
||||||
// 게시물 데이터 로드
|
// 게시물 데이터 로드
|
||||||
const fetchBoardDetails = async () => {
|
const fetchBoardDetails = async () => {
|
||||||
// 수정 데이터 전송
|
|
||||||
let password = accessStore.password;
|
let password = accessStore.password;
|
||||||
const params = {
|
const params = {
|
||||||
password: `${password}` || '',
|
password: `${password}` || '',
|
||||||
};
|
};
|
||||||
//const response = await axios.get(`board/${currentBoardId.value}`);
|
|
||||||
const { data } = await axios.post(`board/${currentBoardId.value}`, params);
|
const { data } = await axios.post(`board/${currentBoardId.value}`, params);
|
||||||
|
|
||||||
if (data.code !== 200) {
|
if (data.code !== 200) {
|
||||||
//toastStore.onToast(data.message, 'e');
|
|
||||||
alert(data.message, 'e');
|
alert(data.message, 'e');
|
||||||
router.back();
|
router.back();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const boardData = data.data;
|
const boardData = data.data;
|
||||||
// 기존 첨부파일 추가
|
|
||||||
if (boardData.hasAttachment && boardData.attachments.length > 0) {
|
if (boardData.hasAttachment && boardData.attachments.length > 0) {
|
||||||
const formatted = addDisplayFileName([...boardData.attachments]);
|
const formatted = addDisplayFileName([...boardData.attachments]);
|
||||||
attachFiles.value = formatted;
|
attachFiles.value = formatted;
|
||||||
originalFiles.value = formatted;
|
originalFiles.value = formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 데이터 설정
|
|
||||||
title.value = boardData.title || '제목 없음';
|
title.value = boardData.title || '제목 없음';
|
||||||
content.value = boardData.content || '내용 없음';
|
content.value = boardData.content || { ops: [] };
|
||||||
originalTitle.value = title.value;
|
originalTitle.value = title.value;
|
||||||
originalContent.value = structuredClone(boardData.content);
|
originalContent.value = structuredClone(boardData.content);
|
||||||
contentInitialized.value = true;
|
contentInitialized.value = true;
|
||||||
@ -242,38 +242,34 @@
|
|||||||
const addDisplayFileName = fileInfos =>
|
const addDisplayFileName = fileInfos =>
|
||||||
fileInfos.map(file => ({
|
fileInfos.map(file => ({
|
||||||
...file,
|
...file,
|
||||||
name: `${file.originalName}.${file.extension}`,
|
name: `${file.originalName}.${file.extension}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 상세 페이지로 이동
|
// 상세 페이지 이동
|
||||||
const goList = () => {
|
const goList = () => {
|
||||||
accessStore.$reset();
|
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();
|
router.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 전 페이지로 이동
|
// 전 페이지 이동
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
accessStore.$reset();
|
accessStore.$reset();
|
||||||
router.back();
|
router.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 유효성 확인
|
// 로컬 유효성 검사 함수 (에디터 내용: 텍스트, 이미지, 비디오 중 하나라도 있으면 유효)
|
||||||
const checkValidation = () => {
|
const isNotValidContent = delta => {
|
||||||
contentAlert.value = $common.isNotValidContent(content);
|
if (!delta?.ops?.length) return true;
|
||||||
titleAlert.value = $common.isNotValidInput(title.value);
|
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);
|
||||||
|
titleAlert.value = $common.isNotValidInput(title.value);
|
||||||
if (titleAlert.value || contentAlert.value || !isFileValid.value) {
|
if (titleAlert.value || contentAlert.value || !isFileValid.value) {
|
||||||
if (titleAlert.value) {
|
if (titleAlert.value) {
|
||||||
title.value = '';
|
title.value = '';
|
||||||
@ -289,7 +285,6 @@
|
|||||||
|
|
||||||
const handleFileUpload = files => {
|
const handleFileUpload = files => {
|
||||||
const validFiles = files.filter(file => file.size <= maxSize);
|
const validFiles = files.filter(file => file.size <= maxSize);
|
||||||
|
|
||||||
if (files.some(file => file.size > maxSize)) {
|
if (files.some(file => file.size > maxSize)) {
|
||||||
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
|
fileError.value = '파일 크기가 10MB를 초과할 수 없습니다.';
|
||||||
return;
|
return;
|
||||||
@ -300,13 +295,11 @@
|
|||||||
}
|
}
|
||||||
fileError.value = '';
|
fileError.value = '';
|
||||||
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
||||||
|
|
||||||
autoIncrement.value++;
|
autoIncrement.value++;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFile = (index, file) => {
|
const removeFile = (index, file) => {
|
||||||
if (file.id) delFileIdx.value.push(file.id);
|
if (file.id) delFileIdx.value.push(file.id);
|
||||||
|
|
||||||
attachFiles.value.splice(index, 1);
|
attachFiles.value.splice(index, 1);
|
||||||
if (attachFiles.value.length <= maxFiles) {
|
if (attachFiles.value.length <= maxFiles) {
|
||||||
fileError.value = '';
|
fileError.value = '';
|
||||||
@ -324,55 +317,41 @@
|
|||||||
};
|
};
|
||||||
////////////////// fileSection[E] ////////////////////
|
////////////////// fileSection[E] ////////////////////
|
||||||
|
|
||||||
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
/** content 변경 감지 (deep 옵션 추가) */
|
||||||
watch(content, () => {
|
watch(content, () => {
|
||||||
contentAlert.value = $common.isNotValidContent(content);
|
contentAlert.value = isNotValidContent(content.value);
|
||||||
});
|
}, { deep: true });
|
||||||
|
|
||||||
// 글 제목 유효성
|
// 글 제목 유효성 검사
|
||||||
const validateTitle = () => {
|
const validateTitle = () => {
|
||||||
titleAlert.value = title.value.trim().length === 0;
|
titleAlert.value = title.value.trim().length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 게시물 수정
|
// 게시물 수정 함수
|
||||||
const updateBoard = async () => {
|
const updateBoard = async () => {
|
||||||
if (checkValidation()) return;
|
if (checkValidation()) return;
|
||||||
|
|
||||||
// 수정 데이터 전송
|
|
||||||
const boardData = {
|
const boardData = {
|
||||||
LOCBRDTTL: title.value.trim(),
|
LOCBRDTTL: title.value.trim(),
|
||||||
LOCBRDCON: JSON.stringify(content.value),
|
LOCBRDCON: JSON.stringify(content.value),
|
||||||
LOCBRDSEQ: currentBoardId.value,
|
LOCBRDSEQ: currentBoardId.value
|
||||||
};
|
};
|
||||||
|
|
||||||
// 업로드 된 첨부파일의 삭제목록
|
|
||||||
if (delFileIdx.value && delFileIdx.value.length > 0) {
|
if (delFileIdx.value && delFileIdx.value.length > 0) {
|
||||||
boardData.delFileIdx = [...delFileIdx.value];
|
boardData.delFileIdx = [...delFileIdx.value];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 에디터에 업로드 된 이미지 인덱스 목록
|
|
||||||
if (editorUploadedImgList.value && editorUploadedImgList.value.length > 0) {
|
if (editorUploadedImgList.value && editorUploadedImgList.value.length > 0) {
|
||||||
boardData.editorUploadedImgList = [...editorUploadedImgList.value];
|
boardData.editorUploadedImgList = [...editorUploadedImgList.value];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 삭제할 에디터 이미지 인덱스
|
|
||||||
if (editorDeleteImgList.value && editorDeleteImgList.value.length > 0) {
|
if (editorDeleteImgList.value && editorDeleteImgList.value.length > 0) {
|
||||||
boardData.editorDeleteImgList = [...editorDeleteImgList.value];
|
boardData.editorDeleteImgList = [...editorDeleteImgList.value];
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileArray = newFileFilter(attachFiles);
|
const fileArray = newFileFilter(attachFiles);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
// formData에 boardData 추가
|
|
||||||
Object.entries(boardData).forEach(([key, value]) => {
|
Object.entries(boardData).forEach(([key, value]) => {
|
||||||
formData.append(key, value);
|
formData.append(key, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// formData에 새로 추가한 파일 추가
|
|
||||||
fileArray.forEach((file, idx) => {
|
fileArray.forEach((file, idx) => {
|
||||||
formData.append('files', file);
|
formData.append('files', file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data } = await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
|
const { data } = await axios.put(`board/${currentBoardId.value}`, formData, { isFormData: true });
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
toastStore.onToast('게시물이 수정되었습니다.', 's');
|
toastStore.onToast('게시물이 수정되었습니다.', 's');
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width: 11%" class="text-center fw-bold">번호</th>
|
<th style="width: 11%" class="text-center fw-bold">번호</th>
|
||||||
<th style="width: 45%" class="text-center fw-bold">제목</th>
|
<th style="width: 45%" class="text-center fw-bold">제목</th>
|
||||||
<th style="width: 10%" class="text-center fw-bold">작성자</th>
|
<th style="width: 10%" class="text-strat fw-bold">작성자</th>
|
||||||
<th style="width: 15%" 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>
|
<th style="width: 9%" class="text-center fw-bold">조회수</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -37,7 +37,9 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- 비밀번호 필드 (익명게시판 선택 시 활성화) -->
|
<!-- 비밀번호 필드 (익명게시판 선택 시 활성화) -->
|
||||||
@ -101,11 +103,14 @@
|
|||||||
@update:deleteImgIndexList="handleDeleteEditorImg"
|
@update:deleteImgIndexList="handleDeleteEditorImg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="mb-4 d-flex justify-content-end">
|
<div class="mb-4 d-flex justify-content-end">
|
||||||
<BackButton @click="goList" />
|
<BackButton @click="goList" />
|
||||||
|
<!-- 저장 버튼은 항상 활성화 -->
|
||||||
<SaveButton @click="write" :isEnabled="isFileValid" />
|
<SaveButton @click="write" :isEnabled="isFileValid" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -115,7 +120,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue';
|
import { ref, onMounted, watch, computed } from 'vue';
|
||||||
import QEditor from '@c/editor/QEditor.vue';
|
import QEditor from '@c/editor/QEditor.vue';
|
||||||
import FormInput from '@c/input/FormInput.vue';
|
import FormInput from '@c/input/FormInput.vue';
|
||||||
import FormFile from '@c/input/FormFile.vue';
|
import FormFile from '@c/input/FormFile.vue';
|
||||||
@ -169,10 +174,12 @@
|
|||||||
|
|
||||||
const fileCount = computed(() => attachFiles.value.length);
|
const fileCount = computed(() => attachFiles.value.length);
|
||||||
|
|
||||||
|
// 업데이트된 에디터 이미지 목록 업데이트
|
||||||
const handleUpdateEditorImg = item => {
|
const handleUpdateEditorImg = item => {
|
||||||
editorUploadedImgList.value = item;
|
editorUploadedImgList.value = item;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 삭제된 에디터 이미지 목록 업데이트
|
||||||
const handleDeleteEditorImg = item => {
|
const handleDeleteEditorImg = item => {
|
||||||
editorDeleteImgList.value = item;
|
editorDeleteImgList.value = item;
|
||||||
};
|
};
|
||||||
@ -187,10 +194,8 @@
|
|||||||
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
fileError.value = `최대 ${maxFiles}개의 파일만 업로드할 수 있습니다.`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileError.value = '';
|
fileError.value = '';
|
||||||
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
attachFiles.value = [...attachFiles.value, ...validFiles].slice(0, maxFiles);
|
||||||
|
|
||||||
autoIncrement.value++;
|
autoIncrement.value++;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -213,7 +218,7 @@
|
|||||||
const validateNickname = () => {
|
const validateNickname = () => {
|
||||||
if (categoryValue.value === 300102) {
|
if (categoryValue.value === 300102) {
|
||||||
nickname.value = nickname.value.replace(/\s/g, ''); // 공백 제거
|
nickname.value = nickname.value.replace(/\s/g, ''); // 공백 제거
|
||||||
nicknameAlert.value = nickname.value.length === 0 ;
|
nicknameAlert.value = nickname.value.length === 0;
|
||||||
} else {
|
} else {
|
||||||
nicknameAlert.value = false;
|
nicknameAlert.value = false;
|
||||||
}
|
}
|
||||||
@ -228,19 +233,28 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* validateContent:
|
||||||
|
* - 내용이 없으면 contentAlert를 true로 설정
|
||||||
|
* - 텍스트, 이미지, 비디오 중 하나라도 존재하면 유효한 콘텐츠로 판단
|
||||||
|
*/
|
||||||
const validateContent = () => {
|
const validateContent = () => {
|
||||||
if (!content.value?.ops?.length) {
|
if (!content.value?.ops?.length) {
|
||||||
contentAlert.value = true;
|
contentAlert.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이미지 포함 여부 확인
|
const hasText = content.value.ops.some(
|
||||||
const hasImage = content.value.ops.some(op => op.insert && typeof op.insert === 'object' && op.insert.image);
|
op => typeof op.insert === 'string' && op.insert.trim().length > 0
|
||||||
// 텍스트 포함 여부 확인
|
);
|
||||||
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
|
||||||
|
);
|
||||||
|
|
||||||
// 텍스트 또는 이미지가 하나라도 있으면 유효한 내용
|
contentAlert.value = !(hasText || hasImage || hasVideo);
|
||||||
contentAlert.value = !(hasText || hasImage);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 글쓰기 */
|
/** 글쓰기 */
|
||||||
@ -264,7 +278,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const boardData = {
|
const boardData = {
|
||||||
LOCBRDTTL: title.value,
|
LOCBRDTTL: title.value.trim(),
|
||||||
LOCBRDCON: JSON.stringify(content.value), // Delta 포맷을 JSON으로 변환
|
LOCBRDCON: JSON.stringify(content.value), // Delta 포맷을 JSON으로 변환
|
||||||
LOCBRDNIC: categoryValue.value === 300102 ? nickname.value : null,
|
LOCBRDNIC: categoryValue.value === 300102 ? nickname.value : null,
|
||||||
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
|
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
|
||||||
@ -294,10 +308,10 @@
|
|||||||
formData.append('CMNFLEORG', fileNameWithoutExt);
|
formData.append('CMNFLEORG', fileNameWithoutExt);
|
||||||
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
||||||
formData.append('CMNFLESIZ', file.size);
|
formData.append('CMNFLESIZ', file.size);
|
||||||
formData.append('file', file); // 📌 실제 파일 추가
|
formData.append('file', file);
|
||||||
|
|
||||||
await axios.post(`board/${boardId}/attachments`, formData, { isFormData: true });
|
await axios.post(`board/${boardId}/attachments`, formData, { isFormData: true });
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,8 +327,8 @@
|
|||||||
router.push('/board');
|
router.push('/board');
|
||||||
};
|
};
|
||||||
|
|
||||||
/** `content` 변경 감지하여 자동 유효성 검사 실행 */
|
/** content 변경 감지 (deep 옵션 추가) */
|
||||||
watch(content, () => {
|
watch(content, () => {
|
||||||
validateContent();
|
validateContent();
|
||||||
});
|
}, { deep: true });
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -65,7 +65,7 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 기존 비밀번호 입력 -->
|
<!-- 기존 비밀번호 입력 -->
|
||||||
<UserFormInput title="기존 비밀번호" name="currentPw" type="password"
|
<UserFormInput title="비밀번호 재설정" placeholder="기존 비밀번호를 입력하세요" name="currentPw" type="password"
|
||||||
:value="password.current" @update:data="password.current = $event"
|
:value="password.current" @update:data="password.current = $event"
|
||||||
@blur="checkCurrentPassword" @keypress="noSpace" />
|
@blur="checkCurrentPassword" @keypress="noSpace" />
|
||||||
<span v-if="passwordError" class="text-danger invalid-feedback mt-1 d-block">
|
<span v-if="passwordError" class="text-danger invalid-feedback mt-1 d-block">
|
||||||
|
|||||||
318
src/views/people/PeopleList.vue
Normal file
318
src/views/people/PeopleList.vue
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
<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>
|
||||||
Loading…
Reference in New Issue
Block a user