localhost-front/src/components/list/ProjectCard.vue
2025-03-24 09:44:44 +09:00

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>