Merge branch 'main' into board-ji

This commit is contained in:
dyhj625 2025-03-27 09:36:28 +09:00
commit 9564788ad8
10 changed files with 193 additions and 116 deletions

View File

@ -24,7 +24,7 @@
class="form-control"
:value="password"
autocomplete="new-password"
maxlength="4"
maxlength="8"
placeholder="비밀번호 입력"
@input="filterInput"
/>

View File

@ -57,7 +57,7 @@
autocomplete="new-password"
v-model="password"
placeholder="비밀번호"
maxlength="4"
maxlength="8"
@input="
password = password.replace(/\s/g, '');
clearAlert('password');

View File

@ -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>

View File

@ -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>

View File

@ -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();
});

View File

@ -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"

View File

@ -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 ||

View File

@ -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 });
}
};

View 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
};
}

View File

@ -30,6 +30,7 @@
autocomplete="new-password"
v-model="password"
placeholder="비밀번호 입력"
maxlength="8"
@input="
password = password.replace(/\s/g, '');
inputCheck();