Merge branch 'main' into board-ji
This commit is contained in:
commit
9564788ad8
@ -24,7 +24,7 @@
|
||||
class="form-control"
|
||||
:value="password"
|
||||
autocomplete="new-password"
|
||||
maxlength="4"
|
||||
maxlength="8"
|
||||
placeholder="비밀번호 입력"
|
||||
@input="filterInput"
|
||||
/>
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
autocomplete="new-password"
|
||||
v-model="password"
|
||||
placeholder="비밀번호"
|
||||
maxlength="4"
|
||||
maxlength="8"
|
||||
@input="
|
||||
password = password.replace(/\s/g, '');
|
||||
clearAlert('password');
|
||||
|
||||
@ -90,9 +90,9 @@ import { useProjectStore } from '@/stores/useProjectStore';
|
||||
import CommuterBtn from '@c/commuters/CommuterBtn.vue';
|
||||
import CommuterProjectList from '@c/commuters/CommuterProjectList.vue';
|
||||
import BackBtn from '@c/button/BackBtn.vue';
|
||||
import flatpickr from 'flatpickr';
|
||||
import monthSelectPlugin from 'flatpickr/dist/plugins/monthSelect/index';
|
||||
import 'flatpickr/dist/plugins/monthSelect/style.css';
|
||||
import { useDatePicker } from '@/stores/useDatePicker';
|
||||
|
||||
const datePickerStore = useDatePicker();
|
||||
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
const user = ref({});
|
||||
@ -115,7 +115,6 @@ const commuters = ref([]);
|
||||
const monthlyCommuters = ref([]);
|
||||
|
||||
const calendarDatepicker = ref(null);
|
||||
let fpInstance = null;
|
||||
|
||||
// 출퇴근 컴포넌트 이벤트 핸들러
|
||||
const handleWorkTimeUpdate = () => {
|
||||
@ -390,7 +389,6 @@ const selectedDateCommuters = computed(() => {
|
||||
commuter.COMMUTDAY === eventDate.value
|
||||
);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
await userStore.userInfo();
|
||||
@ -407,65 +405,13 @@ onMounted(async () => {
|
||||
checkedInProject.value = storedProject;
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
// 달력 데이트피커를 위한 input 요소 동적 생성
|
||||
const datePickerInput = document.createElement('input');
|
||||
datePickerInput.type = 'text';
|
||||
datePickerInput.style.display = 'none';
|
||||
document.body.appendChild(datePickerInput);
|
||||
calendarDatepicker.value = datePickerInput;
|
||||
|
||||
// Flatpickr 초기화 (달 선택)
|
||||
fpInstance = flatpickr(calendarDatepicker.value, {
|
||||
dateFormat: "Y-m",
|
||||
plugins: [
|
||||
new monthSelectPlugin({
|
||||
shorthand: true,
|
||||
dateFormat: "Y-m",
|
||||
altFormat: "F Y"
|
||||
})
|
||||
],
|
||||
onOpen: function() {
|
||||
document.querySelector('.flatpickr-input').style.visibility = 'hidden';
|
||||
},
|
||||
onChange: function(selectedDatesArr, dateStr) {
|
||||
// 선택한 달의 첫날로 달력을 이동
|
||||
fullCalendarRef.value.getApi().gotoDate(dateStr + "-01");
|
||||
const [year, month] = dateStr.split("-");
|
||||
lastRemainingYear.value = parseInt(year, 10);
|
||||
lastRemainingMonth.value = month;
|
||||
loadCalendarData(lastRemainingYear.value, lastRemainingMonth.value);
|
||||
},
|
||||
onClose: function() {
|
||||
calendarDatepicker.value.style.display = "none";
|
||||
datePickerStore.initDatePicker(
|
||||
fullCalendarRef,
|
||||
async (year, month, options) => {
|
||||
// 데이터 다시 불러오기
|
||||
await fetchData();
|
||||
}
|
||||
});
|
||||
|
||||
// FullCalendar 년월월(.fc-toolbar-title) 클릭 시 데이트피커 열기
|
||||
const titleEl = document.querySelector('.fc-toolbar-title');
|
||||
if (titleEl) {
|
||||
titleEl.style.cursor = 'pointer';
|
||||
titleEl.addEventListener('click', () => {
|
||||
const rect = titleEl.getBoundingClientRect();
|
||||
const dpEl = calendarDatepicker.value;
|
||||
|
||||
dpEl.style.display = 'block';
|
||||
dpEl.style.position = 'fixed';
|
||||
dpEl.style.top = `${rect.bottom + window.scrollY}px`;
|
||||
dpEl.style.left = `${rect.left + window.scrollX}px`;
|
||||
dpEl.style.transform = 'translate(-50%, -50%)';
|
||||
dpEl.style.zIndex = '9999';
|
||||
dpEl.style.border = 'none';
|
||||
dpEl.style.outline = 'none';
|
||||
dpEl.style.backgroundColor = 'transparent';
|
||||
// 제목의 중앙 아래에 위치하도록 계산
|
||||
// 또는 CSS transform 사용
|
||||
// dpEl.style.setProperty('--left-position', `${rect.left + window.scrollX}px`);
|
||||
// dpEl.style.transform = 'translateX(-50%)';
|
||||
fpInstance.open();
|
||||
});
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,23 +1,32 @@
|
||||
<template>
|
||||
<div class="commuter-list">
|
||||
<div v-for="post in project" :key="post.PROJCTSEQ"
|
||||
<div
|
||||
v-for="post in sortedProjects"
|
||||
:key="post.PROJCTSEQ"
|
||||
class="border border-2 mt-3 card p-2"
|
||||
:style="`border-color: ${post.projctcolor} !important; color: ${post.projctcolor} !important;`"
|
||||
@dragover="allowDrop($event)"
|
||||
@drop="handleDrop($event, post)">
|
||||
@drop="handleDrop($event, post)"
|
||||
>
|
||||
<p class="mb-1">
|
||||
{{ post.PROJCTNAM }}
|
||||
</p>
|
||||
<div class="row gx-2">
|
||||
<div v-for="commuter in commuters.filter(c => c.PROJCTNAM === post.PROJCTNAM)" :key="commuter.COMMUTCMT" class="col-4">
|
||||
<div
|
||||
v-for="commuter in commuters.filter(c => c.PROJCTNAM === post.PROJCTNAM)"
|
||||
:key="commuter.COMMUTCMT"
|
||||
class="col-4"
|
||||
>
|
||||
<div class="ratio ratio-1x1">
|
||||
<img :src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
||||
<img
|
||||
:src="`${baseUrl}upload/img/profile/${commuter.profile}`"
|
||||
alt="User Profile"
|
||||
class="rounded-circle"
|
||||
:class="isCurrentUser(commuter) ? 'cursor-pointer' : ''"
|
||||
:draggable="isCurrentUser(commuter)"
|
||||
@dragstart="isCurrentUser(commuter) ? dragStart($event, post) : null"
|
||||
@error="$event.target.src = '/img/icons/icon.png'">
|
||||
@error="$event.target.src = '/img/icons/icon.png'"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,7 +35,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { defineProps, defineEmits, computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
@ -57,6 +66,15 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['drop', 'update:selectedProject', 'update:checkedInProject']);
|
||||
|
||||
// 프로젝트 참여인원 순 정렬
|
||||
const sortedProjects = computed(() => {
|
||||
const projectList = Array.isArray(props.project) ? props.project :
|
||||
Object.values(props.project || {});
|
||||
|
||||
return projectList
|
||||
.filter(item => item && typeof item === 'object')
|
||||
.sort((a, b) => (b.participant_count || 0) - (a.participant_count || 0));
|
||||
});
|
||||
// 현재 사용자 확인
|
||||
const isCurrentUser = (commuter) => {
|
||||
return props.user && commuter && commuter.MEMBERSEQ === props.user.id;
|
||||
@ -79,4 +97,6 @@ const handleDrop = (event, targetProject) => {
|
||||
event.preventDefault();
|
||||
emit('drop', { event, targetProject });
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@ -94,7 +94,7 @@
|
||||
<template #title> 프로젝트 수정 </template>
|
||||
<template #body>
|
||||
<FormInput
|
||||
title="이름"
|
||||
title="프로젝트명"
|
||||
name="name"
|
||||
:is-essential="true"
|
||||
:is-alert="nameAlert"
|
||||
@ -292,6 +292,8 @@ const selectedUsers = ref({
|
||||
});
|
||||
|
||||
|
||||
const isKakaoMapLoaded = ref(false);
|
||||
|
||||
const startDateInput = ref(null);
|
||||
const endDateInput = ref(null);
|
||||
|
||||
@ -538,8 +540,10 @@ const handleUpdate = async () => {
|
||||
|
||||
// 주소를 좌표로 변환하는 함수
|
||||
const convertAddressToCoordinates = () => {
|
||||
// kakao maps API가 로드되었는지 확인
|
||||
if (window.kakao && window.kakao.maps) {
|
||||
if (!window.kakao || !window.kakao.maps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const geocoder = new window.kakao.maps.services.Geocoder();
|
||||
geocoder.addressSearch(props.address, (result, status) => {
|
||||
if (status === window.kakao.maps.services.Status.OK) {
|
||||
@ -547,21 +551,8 @@ const convertAddressToCoordinates = () => {
|
||||
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) => {
|
||||
@ -605,8 +596,6 @@ onMounted(async () => {
|
||||
await userStore.userInfo();
|
||||
user.value = userStore.user;
|
||||
|
||||
convertAddressToCoordinates();
|
||||
|
||||
if (startDateInput.value) {
|
||||
// FormInput 내부 input 찾기
|
||||
startInputElement = startDateInput.value.$el.querySelector('input[type="date"]');
|
||||
@ -615,6 +604,14 @@ onMounted(async () => {
|
||||
if (endDateInput.value) {
|
||||
endInputElement = endDateInput.value.$el.querySelector('input[type="date"]');
|
||||
}
|
||||
|
||||
const checkKakaoMapsLoaded = () => {
|
||||
if (window.kakao && window.kakao.maps && window.kakao.maps.services) {
|
||||
convertAddressToCoordinates();
|
||||
}
|
||||
};
|
||||
|
||||
checkKakaoMapsLoaded();
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
<div v-for="post in projectStore.projectList" :key="post.PROJCTSEQ">
|
||||
<ProjectCard
|
||||
:title="post.PROJCTNAM"
|
||||
:description="post.PROJCTDES"
|
||||
:description="post.PROJCTDES ?? ''"
|
||||
:strdate="post.PROJCTSTR"
|
||||
:enddate="post.PROJCTEND"
|
||||
:address="post.PROJCTARR"
|
||||
@ -37,7 +37,7 @@
|
||||
<template #title> 프로젝트 등록 </template>
|
||||
<template #body>
|
||||
<FormInput
|
||||
title="이름"
|
||||
title="프로젝트명"
|
||||
name="name"
|
||||
:is-essential="true"
|
||||
:is-alert="nameAlert"
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
@update:alert="passwordAlert = $event"
|
||||
:value="password"
|
||||
/>
|
||||
<span v-if="passwordError" class="invalid-feedback d-block">{{ passwordError }}</span>
|
||||
|
||||
<UserFormInput
|
||||
title="비밀번호 확인"
|
||||
@ -180,6 +181,7 @@
|
||||
const id = ref('');
|
||||
const idError = ref('');
|
||||
const password = ref('');
|
||||
const passwordError = ref('');
|
||||
const passwordcheck = ref('');
|
||||
const passwordcheckError = ref('');
|
||||
const pwhintRes = ref('');
|
||||
@ -199,6 +201,7 @@
|
||||
const idAlert = ref(false);
|
||||
const idErrorAlert = ref(false);
|
||||
const passwordAlert = ref(false);
|
||||
const passwordErrorAlert = ref(false);
|
||||
const passwordcheckAlert = ref(false);
|
||||
const passwordcheckErrorAlert = ref(false); // 비밀번호 확인 오류 메시지
|
||||
const pwhintResAlert = ref(false);
|
||||
@ -328,11 +331,17 @@
|
||||
}
|
||||
};
|
||||
|
||||
watch(password, (newValue) => {
|
||||
if (newValue.length >= 4) {
|
||||
passwordErrorAlert.value = false;
|
||||
passwordError.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 회원가입
|
||||
const handleSubmit = async () => {
|
||||
await checkColorDuplicate();
|
||||
|
||||
idAlert.value = id.value.trim() === '';
|
||||
passwordAlert.value = password.value.trim() === '';
|
||||
passwordcheckAlert.value = passwordcheck.value.trim() === '';
|
||||
@ -342,6 +351,14 @@
|
||||
addressAlert.value = address.value.trim() === '';
|
||||
phoneAlert.value = phone.value.trim() === '';
|
||||
|
||||
// 비밀번호 길이 체크 로직 추가
|
||||
if (password.value.length < 4) {
|
||||
passwordErrorAlert.value = true;
|
||||
passwordError.value = '비밀번호는 4자리 이상이어야 합니다.';
|
||||
} else {
|
||||
passwordError.value = '';
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(phone.value)) {
|
||||
phoneAlert.value = true;
|
||||
} else {
|
||||
@ -362,6 +379,7 @@
|
||||
idAlert.value ||
|
||||
idErrorAlert.value ||
|
||||
passwordAlert.value ||
|
||||
passwordcErrorAlert.value ||
|
||||
passwordcheckAlert.value ||
|
||||
passwordcheckErrorAlert.value ||
|
||||
pwhintResAlert.value ||
|
||||
|
||||
@ -126,7 +126,7 @@ watch(() => props.data.localVote.total_voted, () => {
|
||||
|
||||
// 종료 체크 함수
|
||||
const checkVoteCompletion = () => {
|
||||
if (props.data.localVote.total_votable === props.data.localVote.total_voted && props.data.localVote.LOCVOTDDT == '') {
|
||||
if (props.data.localVote.total_votable === props.data.localVote.total_voted && props.data.localVote.LOCVOTDDT == null) {
|
||||
emit('voteEnded', { id: props.data.localVote.LOCVOTSEQ });
|
||||
}
|
||||
};
|
||||
|
||||
95
src/stores/useDatePicker.js
Normal file
95
src/stores/useDatePicker.js
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
작성자 : 박지윤
|
||||
작성일 : 2025-03-25
|
||||
수정자 :
|
||||
수정일 :
|
||||
설명 : 달력 데이트 피커
|
||||
*/
|
||||
|
||||
import { ref } from 'vue';
|
||||
import flatpickr from 'flatpickr';
|
||||
import monthSelectPlugin from 'flatpickr/dist/plugins/monthSelect/index';
|
||||
import 'flatpickr/dist/flatpickr.min.css';
|
||||
import 'flatpickr/dist/plugins/monthSelect/style.css';
|
||||
|
||||
export function useDatePicker() {
|
||||
let fpInstance = null;
|
||||
const calendarDatepicker = ref(null);
|
||||
|
||||
const initDatePicker = (fullCalendarRef, onDateChange, options = {}) => {
|
||||
// input 요소 동적 생성
|
||||
const datePickerInput = document.createElement('input');
|
||||
datePickerInput.type = 'text';
|
||||
datePickerInput.style.display = 'none';
|
||||
document.body.appendChild(datePickerInput);
|
||||
calendarDatepicker.value = datePickerInput;
|
||||
|
||||
// Flatpickr 초기화
|
||||
fpInstance = flatpickr(calendarDatepicker.value, {
|
||||
dateFormat: "Y-m",
|
||||
plugins: [
|
||||
new monthSelectPlugin({
|
||||
shorthand: true,
|
||||
dateFormat: "Y-m",
|
||||
altFormat: "F Y"
|
||||
})
|
||||
],
|
||||
onOpen: function() {
|
||||
document.querySelector('.flatpickr-input').style.visibility = 'hidden';
|
||||
},
|
||||
onChange: function(selectedDatesArr, dateStr) {
|
||||
// 선택한 달의 첫날로 달력을 이동
|
||||
if (fullCalendarRef.value) {
|
||||
fullCalendarRef.value.getApi().gotoDate(dateStr + "-01");
|
||||
}
|
||||
|
||||
const [year, month] = dateStr.split("-");
|
||||
|
||||
// onDateChange가 함수인 경우에만 호출
|
||||
if (typeof onDateChange === 'function') {
|
||||
onDateChange(parseInt(year, 10), month, options);
|
||||
}
|
||||
},
|
||||
onClose: function() {
|
||||
if (calendarDatepicker.value) {
|
||||
calendarDatepicker.value.style.display = "none";
|
||||
}
|
||||
},
|
||||
...options
|
||||
});
|
||||
|
||||
// FullCalendar 년월월(.fc-toolbar-title) 클릭 시 데이트피커 열기
|
||||
const titleEl = document.querySelector('.fc-toolbar-title');
|
||||
if (titleEl) {
|
||||
titleEl.style.cursor = 'pointer';
|
||||
titleEl.addEventListener('click', () => {
|
||||
const rect = titleEl.getBoundingClientRect();
|
||||
const dpEl = calendarDatepicker.value;
|
||||
|
||||
dpEl.style.display = 'block';
|
||||
dpEl.style.position = 'fixed';
|
||||
dpEl.style.top = `${rect.bottom + window.scrollY}px`;
|
||||
dpEl.style.left = `${rect.left + window.scrollX}px`;
|
||||
dpEl.style.transform = 'translate(-50%, -50%)';
|
||||
dpEl.style.zIndex = '9999';
|
||||
dpEl.style.border = 'none';
|
||||
dpEl.style.outline = 'none';
|
||||
dpEl.style.backgroundColor = 'transparent';
|
||||
|
||||
fpInstance.open();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const closeDatePicker = () => {
|
||||
if (fpInstance) {
|
||||
fpInstance.close();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
initDatePicker,
|
||||
closeDatePicker,
|
||||
calendarDatepicker
|
||||
};
|
||||
}
|
||||
@ -30,6 +30,7 @@
|
||||
autocomplete="new-password"
|
||||
v-model="password"
|
||||
placeholder="비밀번호 입력"
|
||||
maxlength="8"
|
||||
@input="
|
||||
password = password.replace(/\s/g, '');
|
||||
inputCheck();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user