589 lines
19 KiB
Vue
589 lines
19 KiB
Vue
<template>
|
|
<div class="card mb-3 shadow-sm border" :class="isProjectExpired ? 'end-project' : ''">
|
|
<div class="row g-0">
|
|
<div class="card-body">
|
|
<!-- 제목 -->
|
|
<div class="d-flex justify-content-between ">
|
|
<h5 class="card-title fw-bold">
|
|
{{ title }}
|
|
</h5>
|
|
<div v-if="!isProjectExpired" class="d-flex gap-1">
|
|
<EditBtn @click.stop="openEditModal" />
|
|
<DeleteBtn v-if="isProjectCreator" @click.stop="handleDelete" class="ms-1"/>
|
|
</div>
|
|
</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="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="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="d-flex flex-sm-row align-items-center pb-2">
|
|
<div class="d-flex" @click.stop="isPopoverVisible = !isPopoverVisible">
|
|
<i class="bx bxs-map cursor-pointer" ref="mapIconRef"></i>
|
|
<div class="ms-2">주소</div>
|
|
</div>
|
|
<div class="ms-12 position-relative">
|
|
{{ address }} {{ addressdtail }}
|
|
<!-- 팝오버 -->
|
|
<div v-if="isPopoverVisible" class="position-absolute map ">
|
|
<button type="button" class="btn-close popover-close" @click.stop="isPopoverVisible = !isPopoverVisible"></button>
|
|
<div class="card">
|
|
<div class="card-body p-1">
|
|
<KakaoMap
|
|
v-if="coordinates"
|
|
:lat="coordinates.lat"
|
|
:lng="coordinates.lng"
|
|
class="w-px-250 h-px-200"
|
|
@onLoadKakaoMap="onLoadKakaoMap"
|
|
>
|
|
<KakaoMapMarker
|
|
:lat="coordinates.lat"
|
|
:lng="coordinates.lng"
|
|
/>
|
|
</KakaoMap>
|
|
<div class="position-absolute top-50 translate-middle-y end-0 me-3 z-1 d-flex flex-column gap-1">
|
|
<button class="btn-secondary border-none" @click="zoomOut">+</button>
|
|
<button class="btn-secondary border-none" @click="zoomIn">-</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn ms-auto text-white" :style="`background-color: ${projctColor} !important;`" @click.stop="openModal">log</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 로그 모달 -->
|
|
<CenterModal :display="isModalOpen" @close="closeModal">
|
|
<template #title> Log </template>
|
|
<template #body>
|
|
<div v-if="logData.length > 0">
|
|
<div
|
|
v-for="(log, index) in logData"
|
|
:key="index"
|
|
class="ms-4 mt-2 border p-3"
|
|
>
|
|
<p class="mb-1">{{ log.logDate }}</p>
|
|
<strong>{{ log.logMessage }}</strong>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #footer>
|
|
<BackBtn @click="closeModal" />
|
|
</template>
|
|
</CenterModal>
|
|
|
|
<!-- 수정 모달 -->
|
|
<CenterModal :display="isEditModalOpen" @close="closeEditModal">
|
|
<template #title> 프로젝트 수정 </template>
|
|
<template #body>
|
|
<FormInput
|
|
title="이름"
|
|
name="name"
|
|
:is-essential="true"
|
|
:is-alert="nameAlert"
|
|
:modelValue="selectedProject.PROJCTNAM"
|
|
@update:modelValue="selectedProject.PROJCTNAM = $event"
|
|
@update:alert="nameAlert = $event"
|
|
/>
|
|
|
|
<FormSelect
|
|
title="컬러"
|
|
name="color"
|
|
:is-essential="true"
|
|
:is-label="true"
|
|
:is-common="true"
|
|
:is-color="true"
|
|
:data="allColors"
|
|
:value="selectedProject.PROJCTCOL"
|
|
@update:data="selectedProject.PROJCTCOL = $event"
|
|
/>
|
|
|
|
<div class="mb-2 row">
|
|
<label class="col-md-2 col-form-label">
|
|
참여자
|
|
</label>
|
|
<div class="col-md-10">
|
|
<UserList class="m-0"
|
|
ref="editUserListRef"
|
|
:projctSeq="projctSeq"
|
|
:showOnlyActive="false"
|
|
@user-list-update="handleEditUserListUpdate"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 시작일 -->
|
|
<FormInput
|
|
title="시작일"
|
|
type="date"
|
|
name="startDay"
|
|
:is-essential="true"
|
|
:is-alert="startDayAlert"
|
|
:modelValue="selectedProject.PROJCTSTR"
|
|
@update:modelValue="selectedProject.PROJCTSTR = $event"
|
|
/>
|
|
|
|
<!-- 종료일 -->
|
|
<FormInput
|
|
title="종료일"
|
|
type="date"
|
|
name="endDay"
|
|
:min="selectedProject.PROJCTSTR"
|
|
:modelValue="selectedProject.PROJCTEND"
|
|
@update:modelValue="selectedProject.PROJCTEND = $event"
|
|
/>
|
|
|
|
<FormInput
|
|
title="설명"
|
|
name="description"
|
|
:modelValue="selectedProject.PROJCTDES"
|
|
@update:modelValue="selectedProject.PROJCTDES = $event"
|
|
/>
|
|
|
|
<ArrInput
|
|
title="주소"
|
|
name="address"
|
|
:is-essential="true"
|
|
:is-row="true"
|
|
:modelValue="{
|
|
address: selectedProject.PROJCTARR,
|
|
detailAddress: selectedProject.PROJCTDTL,
|
|
postcode: selectedProject.PROJCTZIP
|
|
}"
|
|
@update:data="updateAddress"
|
|
/>
|
|
</template>
|
|
<template #footer>
|
|
<BackButton @click="closeEditModal" />
|
|
<SaveButton @click="handleUpdate" />
|
|
</template>
|
|
</CenterModal>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { defineProps, onMounted, ref, computed, watch, nextTick } from 'vue';
|
|
import UserList from '@c/user/UserList.vue';
|
|
import CenterModal from '@c/modal/CenterModal.vue';
|
|
import $api from '@api';
|
|
import { KakaoMap, KakaoMapMarker } from 'vue3-kakao-maps';
|
|
import BackBtn from '@c/button/BackBtn.vue';
|
|
import BackButton from '@c/button/BackBtn.vue';
|
|
import SaveButton from '@c/button/SaveBtn.vue';
|
|
import EditBtn from '../button/EditBtn.vue';
|
|
import DeleteBtn from '../button/DeleteBtn.vue';
|
|
import FormInput from '@c/input/FormInput.vue';
|
|
import FormSelect from '@c/input/FormSelect.vue';
|
|
import ArrInput from '@c/input/ArrInput.vue';
|
|
import { useToastStore } from '@s/toastStore';
|
|
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
|
import commonApi, { refreshColorList } from '@/common/commonApi';
|
|
import { useProjectStore } from '@/stores/useProjectStore';
|
|
|
|
// 스토어
|
|
const toastStore = useToastStore();
|
|
const userStore = useUserInfoStore();
|
|
const projectStore = useProjectStore();
|
|
|
|
// Props 정의
|
|
const props = defineProps({
|
|
title: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
strdate: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
enddate: {
|
|
type: String,
|
|
required: true,
|
|
default: "",
|
|
},
|
|
description: {
|
|
type: String,
|
|
required: false,
|
|
default: "",
|
|
},
|
|
address: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
addressdtail: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
addressZip: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
projctSeq: {
|
|
type: Number,
|
|
required: false
|
|
},
|
|
projctCol: {
|
|
type: Number,
|
|
required: false
|
|
},
|
|
projctColor: {
|
|
type: String,
|
|
required: false
|
|
},
|
|
projctCreatorId: {
|
|
type: Number,
|
|
required: false
|
|
},
|
|
resetUserSelection: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
searchParams: {
|
|
type: Object,
|
|
default: () => ({ text: '', year: null })
|
|
}
|
|
});
|
|
|
|
// Emit 정의
|
|
const emit = defineEmits(['update']);
|
|
|
|
// 로그 모달 상태
|
|
const isModalOpen = ref(false);
|
|
const logData = ref([]);
|
|
|
|
// 주소 팝오버 상태
|
|
const isPopoverVisible = ref(false);
|
|
const map = ref();
|
|
const mapIconRef = ref(null);
|
|
const coordinates = ref(null);
|
|
|
|
// 수정 모달 상태
|
|
const isEditModalOpen = ref(false);
|
|
const originalColor = ref('');
|
|
const nameAlert = ref(false);
|
|
const startDayAlert = ref(false);
|
|
const user = ref(null);
|
|
|
|
const editUserListRef = ref(null);
|
|
const userListRef = ref(null);
|
|
|
|
const selectedUsers = ref({
|
|
activeUsers: [],
|
|
disabledUsers: []
|
|
});
|
|
|
|
|
|
// 사용자 목록 업데이트 핸들러
|
|
const handleEditUserListUpdate = (userLists) => {
|
|
selectedUsers.value = userLists;
|
|
};
|
|
|
|
|
|
const isProjectCreator = computed(() => {
|
|
return user.value?.id === props.projctCreatorId;
|
|
});
|
|
|
|
|
|
// 프로젝트 만료 여부 체크 (종료일이 지났는지)
|
|
const isProjectExpired = computed(() => {
|
|
if (!props.enddate) return false;
|
|
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0); // 오늘 날짜의 시작 시간으로 설정
|
|
|
|
const endDate = new Date(props.enddate);
|
|
endDate.setHours(0, 0, 0, 0); // 종료일의 시작 시간으로 설정
|
|
|
|
return endDate < today;
|
|
});
|
|
|
|
// 수정할 프로젝트 데이터
|
|
const selectedProject = ref({
|
|
PROJCTSEQ: props.projctSeq,
|
|
PROJCTNAM: props.title,
|
|
PROJCTSTR: props.strdate,
|
|
PROJCTEND: props.enddate,
|
|
PROJCTZIP: props.addressZip,
|
|
PROJCTARR: props.address,
|
|
PROJCTDTL: props.addressdtail,
|
|
PROJCTDES: props.description,
|
|
PROJCTCOL: props.projctCol,
|
|
projctcolor: props.projctColor,
|
|
});
|
|
|
|
// 컬러 목록 가져오기
|
|
const { colorList } = commonApi({
|
|
loadColor: true,
|
|
colorType: 'YNP',
|
|
});
|
|
|
|
|
|
// 기존 컬러 + 사용 가능 한 컬러
|
|
const allColors = computed(() => {
|
|
// 먼저 기존 컬러 객체를 생성 (이 컬러는 항상 목록에 포함되어야 함)
|
|
const existingColor = {
|
|
value: props.projctCol, // 원래 프로젝트의 컬러 값 사용
|
|
label: props.projctColor // 원래 프로젝트의 컬러 레이블 사용
|
|
};
|
|
|
|
// 중복 제거를 위해 기존 컬러 값과 다른 컬러만 필터링
|
|
const otherColors = colorList.value.filter(color => color.value !== existingColor.value);
|
|
|
|
// 기존 컬러를 첫 번째로 놓고 나머지 컬러 추가
|
|
return [existingColor, ...otherColors];
|
|
});
|
|
|
|
|
|
// 수정 :: 주소
|
|
const updateAddress = addressData => {
|
|
selectedProject.value = {
|
|
...selectedProject.value,
|
|
PROJCTZIP: addressData.postcode,
|
|
PROJCTARR: addressData.address,
|
|
PROJCTDTL: addressData.detailAddress,
|
|
};
|
|
};
|
|
|
|
|
|
// 로그 데이터 가져오기
|
|
const getLogData = async () => {
|
|
const res = await $api.get(`project/log/${props.projctSeq}`);
|
|
|
|
logData.value = res.data.data;
|
|
};
|
|
|
|
// 로그 모달 열기
|
|
const openModal = async () => {
|
|
await getLogData();
|
|
isModalOpen.value = true;
|
|
};
|
|
|
|
// 로그 모달 닫기
|
|
const closeModal = () => {
|
|
isModalOpen.value = false;
|
|
};
|
|
|
|
// 수정 모달 열기
|
|
const openEditModal = async () => {
|
|
selectedProject.value = {
|
|
PROJCTSEQ: props.projctSeq,
|
|
PROJCTNAM: props.title,
|
|
PROJCTSTR: props.strdate,
|
|
PROJCTEND: props.enddate,
|
|
PROJCTZIP: props.addressZip,
|
|
PROJCTARR: props.address,
|
|
PROJCTDTL: props.addressdtail,
|
|
PROJCTDES: props.description,
|
|
PROJCTCOL: props.projctCol,
|
|
projctcolor: props.projctColor,
|
|
};
|
|
|
|
isEditModalOpen.value = true;
|
|
originalColor.value = props.projctCol;
|
|
|
|
};
|
|
|
|
// 수정 모달 닫기
|
|
const closeEditModal = () => {
|
|
selectedProject.value = {
|
|
PROJCTSEQ: props.projctSeq,
|
|
PROJCTNAM: props.title,
|
|
PROJCTSTR: props.strdate,
|
|
PROJCTEND: props.enddate,
|
|
PROJCTZIP: props.addressZip,
|
|
PROJCTARR: props.address,
|
|
PROJCTDTL: props.addressdtail,
|
|
PROJCTDES: props.description,
|
|
PROJCTCOL: props.projctCol,
|
|
projctcolor: props.projctColor,
|
|
};
|
|
|
|
isEditModalOpen.value = false;
|
|
|
|
// UserList의 resetSelection 메서드 호출
|
|
if (editUserListRef.value) {
|
|
editUserListRef.value.resetSelection();
|
|
}
|
|
};
|
|
|
|
// selectedUsers 값 변경 감지
|
|
watch(() => selectedUsers.value.activeUsers, (newVal, oldVal) => {
|
|
}, { deep: true });
|
|
|
|
watch(() => selectedUsers.value.disabledUsers, (newVal, oldVal) => {
|
|
}, { deep: true });
|
|
|
|
// 변경된 내용 있는지 확인
|
|
const hasChanges = computed(() => {
|
|
// 기본 변경 확인 코드
|
|
const basicChanges = selectedProject.value.PROJCTNAM !== props.title ||
|
|
selectedProject.value.PROJCTSTR !== props.strdate ||
|
|
selectedProject.value.PROJCTEND !== props.enddate ||
|
|
selectedProject.value.PROJCTZIP !== props.addressZip ||
|
|
selectedProject.value.PROJCTARR !== props.address ||
|
|
selectedProject.value.PROJCTDTL !== props.addressdtail ||
|
|
selectedProject.value.PROJCTDES !== props.description ||
|
|
selectedProject.value.PROJCTCOL !== props.projctCol;
|
|
|
|
// 사용자 목록 변경 확인
|
|
const userChanges = editUserListRef.value?.hasUserChanges() || false;
|
|
|
|
return basicChanges || userChanges;
|
|
});
|
|
|
|
// 시작일 또는 종료일이 변경될 때 종료일의 최소값을 설정
|
|
watch(
|
|
() => selectedProject.value.PROJCTSTR, // 시작일 (strdate)
|
|
(newStartDate) => {
|
|
if (newStartDate && new Date(newStartDate) > new Date(selectedProject.value.PROJCTEND)) {
|
|
// 시작일이 종료일보다 크면 종료일을 시작일로 설정
|
|
selectedProject.value.PROJCTEND = newStartDate;
|
|
}
|
|
}
|
|
);
|
|
|
|
// resetUserSelection 변경 감지
|
|
watch(() => props.resetUserSelection, () => {
|
|
if (editUserListRef.value) {
|
|
editUserListRef.value.resetSelection();
|
|
}
|
|
});
|
|
|
|
// 프로젝트 수정
|
|
const handleUpdate = async () => {
|
|
nameAlert.value = selectedProject.value.PROJCTNAM.trim() === '';
|
|
startDayAlert.value = selectedProject.value.PROJCTSTR.trim() === '';
|
|
|
|
if (nameAlert.value || startDayAlert.value) {
|
|
return;
|
|
}
|
|
|
|
if (!hasChanges.value) {
|
|
toastStore.onToast('변경된 내용이 없습니다.', 'e');
|
|
return;
|
|
}
|
|
|
|
const disabledMemberSeqs = selectedUsers.value.disabledUsers.map(user => user.MEMBERSEQ);
|
|
|
|
const res = await $api.patch('project/update', {
|
|
projctSeq: selectedProject.value.PROJCTSEQ,
|
|
projctNam: selectedProject.value.PROJCTNAM,
|
|
projctCol: selectedProject.value.PROJCTCOL,
|
|
projctArr: selectedProject.value.PROJCTARR,
|
|
projctDtl: selectedProject.value.PROJCTDTL,
|
|
projctZip: selectedProject.value.PROJCTZIP,
|
|
projctStr: selectedProject.value.PROJCTSTR,
|
|
projctEnd: selectedProject.value.PROJCTEND || null,
|
|
projctDes: selectedProject.value.PROJCTDES || null,
|
|
projctUmb: user.value?.id,
|
|
originalColor: originalColor.value === selectedProject.value.PROJCTCOL ? null : originalColor.value,
|
|
disabledMembers: disabledMemberSeqs
|
|
});
|
|
|
|
if (res.status === 200) {
|
|
toastStore.onToast('수정이 완료 되었습니다.', 's');
|
|
|
|
// 프로젝트 목록 새로고침
|
|
await projectStore.getProjectList(props.searchParams.text, props.searchParams.year, 'false');
|
|
await projectStore.getMemberProjects();
|
|
await refreshColorList('YNP');
|
|
|
|
await editUserListRef.value.fetchProjectParticipation();
|
|
await userListRef.value.fetchProjectParticipation();
|
|
|
|
closeEditModal();
|
|
emit('update', props.searchParams);
|
|
}
|
|
};
|
|
|
|
// 주소를 좌표로 변환하는 함수
|
|
const convertAddressToCoordinates = () => {
|
|
// kakao maps API가 로드되었는지 확인
|
|
if (window.kakao && window.kakao.maps) {
|
|
const geocoder = new window.kakao.maps.services.Geocoder();
|
|
geocoder.addressSearch(props.address, (result, status) => {
|
|
if (status === window.kakao.maps.services.Status.OK) {
|
|
coordinates.value = {
|
|
lat: parseFloat(result[0].y),
|
|
lng: parseFloat(result[0].x)
|
|
};
|
|
} else {
|
|
// 기본 좌표 설정 (본사)
|
|
coordinates.value = {
|
|
lat: 37.2108651707078,
|
|
lng: 127.089445559923
|
|
};
|
|
}
|
|
});
|
|
} else {
|
|
// 기본 좌표로 설정
|
|
coordinates.value = {
|
|
lat: 37.2108651707078,
|
|
lng: 127.089445559923
|
|
};
|
|
}
|
|
};
|
|
|
|
const onLoadKakaoMap = (mapRef) => {
|
|
map.value = mapRef;
|
|
};
|
|
|
|
// 지도 확대
|
|
const zoomIn = () => {
|
|
if (map.value) {
|
|
const level = map.value.getLevel();
|
|
map.value.setLevel(level + 1);
|
|
}
|
|
};
|
|
|
|
// 지도 축소
|
|
const zoomOut = () => {
|
|
if (map.value) {
|
|
const level = map.value.getLevel();
|
|
map.value.setLevel(level - 1);
|
|
}
|
|
};
|
|
|
|
// 프로젝트 삭제
|
|
const handleDelete = () => {
|
|
$api.patch('project/delete', {
|
|
projctSeq: props.projctSeq,
|
|
projctCol: props.projctCol,
|
|
})
|
|
.then(res => {
|
|
if (res.status === 200) {
|
|
toastStore.onToast('삭제가 완료되었습니다.', 's');
|
|
projectStore.getProjectList();
|
|
projectStore.getMemberProjects();
|
|
}
|
|
})
|
|
};
|
|
|
|
// 컴포넌트 마운트 시 실행
|
|
onMounted(async () => {
|
|
// 사용자 정보 가져오기
|
|
await userStore.userInfo();
|
|
user.value = userStore.user;
|
|
|
|
convertAddressToCoordinates();
|
|
});
|
|
|
|
|
|
</script>
|
|
|