This commit is contained in:
khj0414 2025-02-27 14:48:55 +09:00
commit f248141108
17 changed files with 584 additions and 478 deletions

View File

@ -1,6 +1,2 @@
/* 여기에 dark css 작성 */ /* 여기에 dark css 작성 */
.display-block {
display: block !important;
}

View File

@ -1,11 +1,6 @@
/* 여기에 light css 작성 */ /* 여기에 light css 작성 */
.display-block {
display: block !important;
}
/* 휴가 */ /* 휴가 */
.fc-daygrid-day-events { .fc-daygrid-day-events {
@ -111,11 +106,7 @@ opacity: 0.6; /* 흐려 보이게 */
border-radius: 5px; border-radius: 5px;
background: #f9f9f9; background: #f9f9f9;
} }
/* 모달 본문 스크롤 */
.modal-body {
max-height: 130px;
overflow-y: auto;
}
/* 선물하기 모달 */ /* 선물하기 모달 */
@ -162,30 +153,6 @@ opacity: 0.6; /* 흐려 보이게 */
color: #ff0800; /* 호버 시 아이콘 색상 변경 */ color: #ff0800; /* 호버 시 아이콘 색상 변경 */
} }
/* 모달 배경 투명하게 */
.modal-dialog {
background: none !important; /* 배경 제거 */
box-shadow: none !important; /* 음영 제거 */
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
/* 모달 내용 스타일 */
.modal-content {
background: #fff; /* 기존 흰색 배경 유지 */
border-radius: 8px;
box-shadow: none !important; /* 내부 음영 제거 */
padding: 20px;
max-width: 500px;
width: 100%;
}
.grayscaleImg { .grayscaleImg {
filter: grayscale(100%); filter: grayscale(100%);
} }

File diff suppressed because one or more lines are too long

View File

@ -40,39 +40,39 @@ const common = {
return null; // Delta 객체가 아니거나 ops가 없을 경우 null 반환 return null; // Delta 객체가 아니거나 ops가 없을 경우 null 반환
}, },
// /** /**
// * Date 타입 문자열 포멧팅 * Date 타입 문자열 포멧팅
// * *
// * @param {string} dateStr * @param {string} dateStr
// * @return * @return
// * 1. Date type 인 경우 예시 '25-02-24 12:02' * 1. Date type 경우 예시 '25-02-24 12:02'
// * 2. Date type 이 아닌 경우 입력값 리턴 * 2. Date type 아닌 경우 입력값 리턴
// * *
// */ */
// dateFormatter(dateStr) { dateFormatter(dateStr) {
// const date = new Date(dateStr); const date = new Date(dateStr);
// const dateCheck = date.getTime(); const dateCheck = date.getTime();
// if (isNaN(dateCheck)) { if (isNaN(dateCheck)) {
// return dateStr; return dateStr;
// } else { } else {
// const { year, month, day, hours, minutes } = this.formatDateTime(date); const { year, month, day, hours, minutes } = this.formatDateTime(date);
// return `${year}-${month}-${day} ${hours}:${minutes}`; return `${year}-${month}-${day} ${hours}:${minutes}`;
// } }
// }, },
// formatDateTime(date) { formatDateTime(date) {
// const zeroFormat = num => (num < 10 ? `0${num}` : num); const zeroFormat = num => (num < 10 ? `0${num}` : num);
// return { return {
// year: date.getFullYear(), year: date.getFullYear(),
// month: zeroFormat(date.getMonth() + 1), month: zeroFormat(date.getMonth() + 1),
// day: zeroFormat(date.getDate()), day: zeroFormat(date.getDate()),
// hours: zeroFormat(date.getHours()), hours: zeroFormat(date.getHours()),
// minutes: zeroFormat(date.getMinutes()), minutes: zeroFormat(date.getMinutes()),
// seconds: zeroFormat(date.getSeconds()), seconds: zeroFormat(date.getSeconds()),
// }; };
// }, },
}; };
export default { export default {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mb-2 row" > <div class="mb-2 row">
<div class="d-flex"> <div class="d-flex">
<label :for="name" class="col-md-2 col-form-label"> <label :for="name" class="col-md-2 col-form-label">
{{ title }} {{ title }}
@ -28,7 +28,6 @@
placeholder="기본주소" placeholder="기본주소"
readonly readonly
/> />
</div> </div>
<div> <div>
@ -87,7 +86,11 @@ const props = defineProps({
}, },
modelValue: { modelValue: {
type: Object, type: Object,
default: () => ({}), default: () => ({
postcode: '',
address: '',
detailAddress: ''
}),
required: false required: false
} }
}); });
@ -95,13 +98,13 @@ const props = defineProps({
// watch // watch
watch(() => props.modelValue, (newValue) => { watch(() => props.modelValue, (newValue) => {
if (newValue) { if (newValue) {
postcode.value = newValue.PROJCTZIP || ''; postcode.value = newValue.postcode || '';
address.value = newValue.PROJCTARR || ''; address.value = newValue.address || '';
detailAddress.value = newValue.PROJCTDTL || ''; detailAddress.value = newValue.detailAddress || '';
} }
}, { immediate: true }); }, { immediate: true });
const emits = defineEmits(['update:data', 'update:alert']); const emits = defineEmits(['update:data', 'update:alert', 'update:modelValue']);
// //
const openAddressSearch = () => { const openAddressSearch = () => {
@ -136,6 +139,7 @@ const emitAddressData = () => {
detailAddress: detailAddress.value, detailAddress: detailAddress.value,
}; };
emits('update:data', fullAddress); emits('update:data', fullAddress);
emits('update:modelValue', fullAddress); // modelValue
}; };
// isAlert false // isAlert false

View File

@ -16,12 +16,12 @@
:min="min" :min="min"
@focusout="$emit('focusout', modelValue)" @focusout="$emit('focusout', modelValue)"
/> />
<div class="invalid-feedback" :class="isAlert ? 'display-block' : ''"> <div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">
{{ title }} 확인해주세요. {{ title }} 확인해주세요.
</div> </div>
<!-- 카테고리 중복 --> <!-- 카테고리 중복 -->
<div class="invalid-feedback" :class="isCateAlert ? 'display-block' : ''"> <div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">
카테고리중복이거나 공백이면 안됩니다. 확인해주세요. 카테고리 중복입니다.
</div> </div>
</div> </div>
</div> </div>
@ -83,7 +83,8 @@ const props = defineProps({
}); });
// Emits // Emits
const emits = defineEmits(['update:modelValue', 'focusout']); const emits = defineEmits(['update:modelValue', 'focusout', 'update:alert']);
// `inputValue` // `inputValue`
const inputValue = ref(props.modelValue); const inputValue = ref(props.modelValue);
@ -91,6 +92,11 @@ const inputValue = ref(props.modelValue);
// //
watch(inputValue, (newValue) => { watch(inputValue, (newValue) => {
emits('update:modelValue', newValue); emits('update:modelValue', newValue);
// `alert` false
if (newValue.trim() !== '') {
emits('update:alert', false);
}
}); });
// //
@ -99,10 +105,7 @@ watch(() => props.modelValue, (newValue) => {
inputValue.value = newValue; inputValue.value = newValue;
} }
}); });
</script> </script>
<style>
.none {
display: none;
}
</style>

View File

@ -3,8 +3,13 @@
<div class="row g-0"> <div class="row g-0">
<div class="card-body"> <div class="card-body">
<!-- 제목 --> <!-- 제목 -->
<h5 class="card-title"> <h5 class="card-title d-flex justify-content-between">
{{ title }} {{ title }}
<div>
<EditBtn @click.stop="openEditModal" />
<DeleteBtn @click.stop="handleDelete" class="ms-1"/>
</div>
</h5> </h5>
<!-- 날짜 --> <!-- 날짜 -->
<div class="d-flex flex-column flex-sm-row align-items-center pb-2"> <div class="d-flex flex-column flex-sm-row align-items-center pb-2">
@ -27,29 +32,23 @@
<!-- 주소 --> <!-- 주소 -->
<div class="d-flex flex-column flex-sm-row align-items-center pb-2"> <div class="d-flex flex-column flex-sm-row align-items-center pb-2">
<div class="d-flex" @click.stop="isPopoverVisible = !isPopoverVisible"> <div class="d-flex" @click.stop="isPopoverVisible = !isPopoverVisible">
<i <i class="bx bxs-map cursor-pointer" ref="mapIconRef"></i>
class="bx bxs-map cursor-pointer"
ref="mapIconRef"
></i>
<div class="ms-2">주소</div> <div class="ms-2">주소</div>
</div> </div>
<div class="ms-12 position-relative"> <div class="ms-12 position-relative">
{{ address }} {{ addressdtail }} {{ address }} {{ addressdtail }}
<!-- 팝오버 --> <!-- 팝오버 -->
<div <div v-if="isPopoverVisible" class="position-absolute w-100 map text-end">
v-if="isPopoverVisible" <button type="button" class="btn-close popover-close" @click.stop="isPopoverVisible = !isPopoverVisible"></button>
class="position-absolute w-100 map text-end"
@click.stop
>
<button type="button" class="btn-close popover-close" @click.stop="isPopoverVisible = !isPopoverVisible"></button>
<div class="card"> <div class="card">
<div class="card-body p-1"> <div class="card-body p-1">
<KakaoMap <KakaoMap
v-if="coordinates" v-if="coordinates"
:lat="coordinates.lat" :lat="coordinates.lat"
:lng="coordinates.lng" :lng="coordinates.lng"
:draggable="false"
class="w-100 h-px-200" class="w-100 h-px-200"
> >
<KakaoMapMarker <KakaoMapMarker
:lat="coordinates.lat" :lat="coordinates.lat"
:lng="coordinates.lng" :lng="coordinates.lng"
@ -59,39 +58,122 @@
</div> </div>
</div> </div>
</div> </div>
<button type="button" class="btn ms-auto text-white" :style="`background-color: ${projctCol} !important;`" @click.stop="openModal">log</button> <button type="button" class="btn ms-auto text-white" :style="`background-color: ${projctColor} !important;`" @click.stop="openModal">log</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 로그 모달 -->
<CenterModal :display="isModalOpen" @close="closeModal"> <CenterModal :display="isModalOpen" @close="closeModal">
<template #title> Log </template> <template #title> Log </template>
<template #body> <template #body>
<div class="ms-4 mt-2 border p-3" v-if="logData"> <div v-if="logData.length > 0">
<p class="mb-1">{{ logData.createDate }}</p> <div
<strong>[{{ logData.creator }}] 프로젝트 등록</strong> v-for="(log, index) in logData"
</div> :key="index"
class="ms-4 mt-2 border p-3"
<div class="log-item" v-if="logData?.updateDate"> >
<div class="ms-4 mt-2 border p-3"> <p class="mb-1">{{ log.logDate }}</p>
<p class="mb-1">{{ logData.updateDate }}</p> <strong>{{ log.logMessage }}</strong>
<strong>[{{ logData.updater }}] 프로젝트 수정</strong>
</div> </div>
</div> </div>
</template> </template>
<template #footer> <template #footer>
<button type="button" class="btn btn-secondary" @click="closeModal">닫기</button> <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"
:data="allColors"
:value="selectedProject.PROJCTCOL"
@update:data="selectedProject.PROJCTCOL = $event"
/>
<FormInput
title="시작일"
type="date"
name="startDay"
:is-essential="true"
:modelValue="selectedProject.PROJCTSTR"
@update:modelValue="selectedProject.PROJCTSTR = $event"
/>
<FormInput
title="종료일"
type="date"
name="endDay"
: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> </template>
</CenterModal> </CenterModal>
</template> </template>
<script setup> <script setup>
import { defineProps, onMounted, ref } from 'vue'; import { defineProps, onMounted, ref, computed, inject } from 'vue';
import UserList from '@c/user/UserList.vue'; import UserList from '@c/user/UserList.vue';
import CenterModal from '@c/modal/CenterModal.vue'; import CenterModal from '@c/modal/CenterModal.vue';
import $api from '@api'; import $api from '@api';
import { KakaoMap, KakaoMapMarker } from 'vue3-kakao-maps'; 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 from '@/common/commonApi';
//
const toastStore = useToastStore();
const userStore = useUserInfoStore();
// Props // Props
const props = defineProps({ const props = defineProps({
@ -120,39 +202,144 @@ const props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
addressZip: {
type: String,
required: true,
},
projctSeq: { projctSeq: {
type: Number, type: Number,
required: false required: false
}, },
projctCol: { projctCol: {
type: Number,
required: false
},
projctColor: {
type: String, type: String,
required: false required: false
}, },
}); });
defineEmits(['click']); // Emit
const emit = defineEmits(['update']);
//
const isModalOpen = ref(false); const isModalOpen = ref(false);
const logData = ref(null); const logData = ref([]);
//
const isPopoverVisible = ref(false); const isPopoverVisible = ref(false);
const mapIconRef = ref(null); const mapIconRef = ref(null);
const coordinates = ref(null); const coordinates = ref(null);
const fetchLogData = async () => { //
const response = await $api.get(`project/log/${props.projctSeq}`); const isEditModalOpen = ref(false);
logData.value = response.data.data.length > 0 ? response.data.data[0] : {}; const originalColor = ref('');
const nameAlert = ref(false);
const user = ref(null);
//
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: selectedProject.value.PROJCTCOL, label: selectedProject.value.projctcolor };
return [existingColor, ...colorList.value];
});
// ::
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 () => { const openModal = async () => {
await fetchLogData(); await getLogData();
isModalOpen.value = true; isModalOpen.value = true;
}; };
//
const closeModal = () => { const closeModal = () => {
isModalOpen.value = false; isModalOpen.value = false;
}; };
//
const openEditModal = () => {
isEditModalOpen.value = true;
originalColor.value = props.projctCol;
// ( )
if (!user.value) {
userStore.userInfo().then(() => {
user.value = userStore.user;
});
}
};
//
const closeEditModal = () => {
isEditModalOpen.value = false;
};
//
const handleUpdate = () => {
nameAlert.value = selectedProject.value.PROJCTNAM.trim() === '';
if (nameAlert.value) {
return;
}
$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?.name,
originalColor: originalColor.value === selectedProject.value.PROJCTCOL ? null : originalColor.value,
}).then(res => {
if (res.status === 200) {
toastStore.onToast('수정이 완료 되었습니다.', 's');
closeEditModal();
//
emit('update');
window.location.reload()
}
});
};
// //
const convertAddressToCoordinates = () => { const convertAddressToCoordinates = () => {
@ -174,11 +361,27 @@ const convertAddressToCoordinates = () => {
}); });
}; };
// //
onMounted(() => { const handleDelete = () => {
$api.patch('project/delete', {
projctSeq: props.projctSeq,
projctCol: props.projctCol,
})
.then(res => {
if (res.status === 200) {
toastStore.onToast('삭제가 완료되었습니다.', 's');
location.reload()
}
})
};
//
onMounted(async () => {
convertAddressToCoordinates(); convertAddressToCoordinates();
//
await userStore.userInfo();
user.value = userStore.user;
}); });
</script> </script>

View File

@ -1,13 +1,12 @@
<template> <template>
<div @click="closeModal" class="modal fade scrollbar-none" :class="{ 'show': display, 'display-block': display , 'modal-back' : display }" id="modalCenter" tabindex="-1" aria-modal="true" role="dialog"> <div @click="closeModal" class="modal fade scrollbar-none" :class="{ 'show': display , 'd-block': display , 'bg-dark bg-opacity-50' : display }" id="modalCenter" tabindex="-1" aria-modal="true" role="dialog">
<div @click.stop class="modal-dialog modal-dialog-centered" role="document"> <div @click.stop class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title m-auto fw-bold" id="modalCenterTitle"> <h5 class="modal-title m-auto fw-bold" id="modalCenterTitle">
<slot name="title">Modal Title</slot> <slot name="title">Modal Title</slot>
</h5> </h5>
<button type="button" class="btn-close" @click="closeModal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<slot name="body">Modal body</slot> <slot name="body">Modal body</slot>
@ -21,26 +20,31 @@
</template> </template>
<script setup> <script setup>
const prop = defineProps({ const prop = defineProps({
display : { display : {
type: Boolean, type: Boolean,
default: false, default: false,
required: true, required: true,
}, },
create: {
type: Boolean,
default: false,
}
}); });
const emit = defineEmits(['close']); const emit = defineEmits(['close' , 'reset']);
const closeModal = () => { const closeModal = () => {
emit('close' , false); if (prop.create) {
emit('reset');
}
emit('close', false);
}; };
</script> </script>
<style>
.modal-back {
background: rgba(0, 0, 0, 0.5);
}
</style>

View File

@ -122,5 +122,24 @@
<style scoped> <style scoped>
/* 모달 배경 투명하게 */
.modal-dialog {
background: none !important; /* 배경 제거 */
box-shadow: none !important; /* 음영 제거 */
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
/* 모달 내용 스타일 */
.modal-content {
background: #fff; /* 기존 흰색 배경 유지 */
border-radius: 8px;
box-shadow: none !important; /* 내부 음영 제거 */
padding: 20px;
max-width: 500px;
width: 100%;
}
</style> </style>

View File

@ -138,4 +138,25 @@ const closeModal = () => {
</script> </script>
<style scoped> <style scoped>
/* 모달 배경 투명하게 */
.modal-dialog {
background: none !important; /* 배경 제거 */
box-shadow: none !important; /* 음영 제거 */
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
/* 모달 내용 스타일 */
.modal-content {
background: #fff; /* 기존 흰색 배경 유지 */
border-radius: 8px;
box-shadow: none !important; /* 내부 음영 제거 */
padding: 20px;
max-width: 500px;
width: 100%;
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<SearchBar @update:data="search"/> <SearchBar @update:data="search" />
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<CategoryBtn :lists="yearCategory" @update:data="selectedCategory = $event" /> <CategoryBtn :lists="yearCategory" @update:data="selectedCategory = $event" />
<WriteBtn class="mt-2 ms-auto" @click="openCreateModal" /> <WriteBtn class="mt-2 ms-auto" @click="openCreateModal" />
@ -11,7 +11,7 @@
<p class="text-muted mt-4">등록된 프로젝트가 없습니다.</p> <p class="text-muted mt-4">등록된 프로젝트가 없습니다.</p>
</div> </div>
<div v-for="post in projectStore.projectList" :key="post.PROJCTSEQ" @click="openEditModal(post)" class="cursor-pointer"> <div v-for="post in projectStore.projectList" :key="post.PROJCTSEQ">
<ProjectCard <ProjectCard
:title="post.PROJCTNAM" :title="post.PROJCTNAM"
:description="post.PROJCTDES" :description="post.PROJCTDES"
@ -19,358 +19,239 @@
:enddate="post.PROJCTEND" :enddate="post.PROJCTEND"
:address="post.PROJCTARR" :address="post.PROJCTARR"
:addressdtail="post.PROJCTDTL" :addressdtail="post.PROJCTDTL"
:addressZip="post.PROJCTZIP"
:projctSeq="post.PROJCTSEQ" :projctSeq="post.PROJCTSEQ"
:projctCol="post.projctcolor" :projctCol="post.PROJCTCOL"
:projctColor="post.projctcolor"
@update="getProjectList"
/> />
</div> </div>
</div> </div>
<!-- 등록 모달 --> <!-- 등록 모달 -->
<CenterModal :display="isCreateModalOpen" @close="closeCreateModal"> <form @reset.prevent="formReset">
<template #title> 프로젝트 등록 </template> <CenterModal :display="isCreateModalOpen" @close="closeCreateModal" :create="true" @reset="formReset">
<template #body> <template #title> 프로젝트 등록 </template>
<FormInput <template #body>
title="이름" <FormInput
name="name" title="이름"
:is-essential="true" name="name"
:is-alert="nameAlert" :is-essential="true"
@update:modelValue="name = $event" :is-alert="nameAlert"
/> :modelValue="name"
@update:alert="nameAlert = $event"
@update:modelValue="name = $event"
/>
<FormSelect <FormSelect
title="컬러" title="컬러"
name="color" name="color"
:is-essential="true" :is-essential="true"
:is-label="true" :is-label="true"
:is-common="true" :is-common="true"
:data="colorList" :data="colorList"
@update:data="color = $event" @update:data="color = $event"
/> />
<FormInput <FormInput
title="시작 일" title="시작 일"
type="date" name="startDay"
name="startDay" :type="'date'"
v-model="startDay" :is-essential="true"
:is-essential="true" :modelValue="startDay"
/> v-model="startDay"
/>
<FormInput <FormInput
title="종료 일" title="종료 일"
name="endDay" :type="'date'"
:type="'date'" name="endDay"
@update:modelValue="endDay = $event" :modelValue="endDay"
/> @update:modelValue="endDay = $event"
/>
<FormInput <FormInput
title="설명" title="설명"
name="description" name="description"
@update:modelValue="description = $event" :modelValue="description"
/> @update:modelValue="description = $event"
/>
<ArrInput <ArrInput
title="주소" title="주소"
name="address" name="address"
:isEssential="true" :isEssential="true"
:is-row="true" :is-row="true"
:is-alert="addressAlert" :is-alert="addressAlert"
@update:data="handleAddressUpdate" :modelValue="addressData"
@update:alert="addressAlert = $event" @update:data="handleAddressUpdate"
/> @update:alert="addressAlert = $event"
</template> />
<template #footer> </template>
<BackButton @click="closeCreateModal" /> <template #footer>
<SaveButton @click="handleCreate" /> <BackButton type="reset" @click="closeCreateModal" />
</template> <SaveButton @click="handleCreate" />
</CenterModal> </template>
</CenterModal>
<!-- 수정 모달 --> </form>
<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"
/>
<FormSelect
title="컬러"
name="color"
:is-essential="true"
:is-label="true"
:is-common="true"
:data="allColors"
:value="selectedProject.PROJCTCOL"
@update:data="selectedProject.PROJCTCOL = $event"
/>
<FormInput
title="시작일"
type="date"
name="startDay"
:is-essential="true"
:modelValue="selectedProject.PROJCTSTR"
@update:modelValue="selectedProject.PROJCTSTR = $event"
/>
<FormInput
title="종료일"
type="date"
name="endDay"
: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="selectedProject"
@update:data="updateAddress"
/>
</template>
<template #footer>
<BackButton @click="closeEditModal" />
<SaveButton @click="handleUpdate" />
</template>
</CenterModal>
</template> </template>
<script setup> <script setup>
import { computed, inject, ref, watch, onMounted } from 'vue'; import { computed, ref, watch, onMounted, inject } from 'vue';
import SearchBar from '@c/search/SearchBar.vue'; import SearchBar from '@c/search/SearchBar.vue';
import ProjectCard from '@c/list/ProjectCard.vue'; import ProjectCard from '@c/list/ProjectCard.vue';
import CategoryBtn from '@c/category/CategoryBtn.vue'; import CategoryBtn from '@c/category/CategoryBtn.vue';
import WriteBtn from '@c/button/WriteBtn.vue'; import WriteBtn from '@c/button/WriteBtn.vue';
import CenterModal from '@c/modal/CenterModal.vue'; import CenterModal from '@c/modal/CenterModal.vue';
import FormSelect from '@c/input/FormSelect.vue'; import FormSelect from '@c/input/FormSelect.vue';
import FormInput from '@c/input/FormInput.vue'; import FormInput from '@c/input/FormInput.vue';
import ArrInput from '@c/input/ArrInput.vue'; import ArrInput from '@c/input/ArrInput.vue';
import commonApi from '@/common/commonApi'; import commonApi from '@/common/commonApi';
import { useToastStore } from '@s/toastStore'; import { useToastStore } from '@s/toastStore';
import { useUserInfoStore } from '@/stores/useUserInfoStore'; import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useProjectStore } from '@/stores/useProjectStore'; import { useProjectStore } from '@/stores/useProjectStore';
import $api from '@api'; import $api from '@api';
import SaveButton from '@c/button/SaveBtn.vue'; import SaveButton from '@c/button/SaveBtn.vue';
import BackButton from '@c/button/BackBtn.vue' import BackButton from '@c/button/BackBtn.vue';
const dayjs = inject('dayjs'); const toastStore = useToastStore();
const today = dayjs().format('YYYY-MM-DD'); const userStore = useUserInfoStore();
const toastStore = useToastStore(); const projectStore = useProjectStore();
const userStore = useUserInfoStore();
const projectStore = useProjectStore();
// //
const user = ref(null); const user = ref(null);
const selectedCategory = ref(null); const selectedCategory = ref(null);
const searchText = ref(''); const searchText = ref('');
// // dayjs
const isCreateModalOpen = ref(false); const dayjs = inject('dayjs');
const name = ref('');
const color = ref('');
const address = ref('');
const detailAddress = ref('');
const postcode = ref('');
const startDay = ref(today);
const endDay = ref('');
const description = ref('');
const nameAlert = ref(false);
const addressAlert = ref(false);
// // YYYY-MM-DD
const isEditModalOpen = ref(false); const today = dayjs().format('YYYY-MM-DD');
const originalColor = ref('');
const selectedProject = ref({
PROJCTSEQ: '',
PROJCTNAM: '',
PROJCTSTR: '',
PROJCTEND: '',
PROJCTZIP: '',
PROJCTARR: '',
PROJCTDTL: '',
PROJCTDES: '',
PROJCTCOL: '',
projctcolor: '',
});
// API //
const { yearCategory, colorList } = commonApi({ const isCreateModalOpen = ref(false);
loadColor: true, const name = ref('');
colorType: 'YNP', const color = ref('');
loadYearCategory: true,
});
// const startDay = ref(today);
const search = async (searchKeyword) => { const endDay = ref('');
searchText.value = searchKeyword.trim(); const description = ref('');
await getProjectList(); const nameAlert = ref(false);
}; const addressAlert = ref(false);
const selectedYear = computed(() => { const addressData = ref({
if (!selectedCategory.value || selectedCategory.value === 900101) { postcode: '',
return null; address: '',
} detailAddress: ''
// category label
return yearCategory.value.find(item => item.value === selectedCategory.value)?.label || null;
});
//
const getProjectList = async () => {
await projectStore.getProjectList(searchText.value, selectedYear.value);
};
//
watch(selectedCategory, async () => {
await getProjectList();
});
//
const openCreateModal = () => {
isCreateModalOpen.value = true;
};
const closeCreateModal = () => {
isCreateModalOpen.value = false;
resetCreateForm();
};
const resetCreateForm = () => {
name.value = '';
color.value = '';
address.value = '';
detailAddress.value = '';
postcode.value = '';
startDay.value = today;
endDay.value = '';
description.value = '';
nameAlert.value = false;
addressAlert.value = false;
};
// ::
const handleAddressUpdate = addressData => {
address.value = addressData.address;
detailAddress.value = addressData.detailAddress;
postcode.value = addressData.postcode;
};
//
const handleCreate = async () => {
nameAlert.value = name.value.trim() === '';
addressAlert.value = address.value.trim() === '';
if (nameAlert.value || addressAlert.value) {
return;
}
$api.post('project/insert', {
projctNam: name.value,
projctCol: color.value,
projctStr: startDay.value,
projctEnd: endDay.value || null,
projctDes: description.value || null,
projctArr: address.value,
projctDtl: detailAddress.value,
projctZip: postcode.value,
projctCmb: user.value.name,
})
.then(res => {
if (res.status === 200) {
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
closeCreateModal();
getProjectList();
}
}); });
};
// // API
const openEditModal = (post) => { const { yearCategory, colorList } = commonApi({
isEditModalOpen.value = true; loadColor: true,
selectedProject.value = { ...post }; colorType: 'YNP',
originalColor.value = post.PROJCTCOL; loadYearCategory: true,
}; });
const closeEditModal = () => { //
isEditModalOpen.value = false; const search = async searchKeyword => {
}; searchText.value = searchKeyword.trim();
await getProjectList();
// +
const allColors = computed(() => {
const existingColor = { value: selectedProject.value.PROJCTCOL, label: selectedProject.value.projctcolor };
return [existingColor, ...colorList.value];
});
//
const hasChanges = computed(() => {
const original = projectStore.projectList.find(p => p.PROJCTSEQ === selectedProject.value.PROJCTSEQ);
if (!original) return false;
return (
original.PROJCTNAM !== selectedProject.value.PROJCTNAM ||
original.PROJCTCOL !== selectedProject.value.PROJCTCOL ||
original.PROJCTARR !== selectedProject.value.PROJCTARR ||
original.PROJCTDTL !== selectedProject.value.PROJCTDTL ||
original.PROJCTZIP !== selectedProject.value.PROJCTZIP ||
original.PROJCTSTR !== selectedProject.value.PROJCTSTR ||
original.PROJCTEND !== selectedProject.value.PROJCTEND ||
original.PROJCTDES !== selectedProject.value.PROJCTDES
);
});
// ::
const updateAddress = (addressData) => {
selectedProject.value = {
...selectedProject.value,
PROJCTZIP: addressData.postcode,
PROJCTARR: addressData.address,
PROJCTDTL: addressData.detailAddress
}; };
};
// const selectedYear = computed(() => {
const handleUpdate = () => { if (!selectedCategory.value || selectedCategory.value === 900101) {
if (!hasChanges.value) { return null;
toastStore.onToast('변경된 내용이 없습니다.', 'e'); }
return; // category label
return yearCategory.value.find(item => item.value === selectedCategory.value)?.label || null;
});
//
const getProjectList = async () => {
await projectStore.getProjectList(searchText.value, selectedYear.value);
};
//
watch(selectedCategory, async () => {
await getProjectList();
});
//
const openCreateModal = () => {
isCreateModalOpen.value = true;
};
const closeCreateModal = () => {
isCreateModalOpen.value = false;
};
const formReset = () => {
name.value = '';
color.value = '';
addressData.value = {
postcode: '',
address: '',
detailAddress: ''
};
startDay.value = today;
endDay.value = '';
description.value = '';
nameAlert.value = false;
addressAlert.value = false;
} }
$api.patch('project/update', { // ::
projctSeq: selectedProject.value.PROJCTSEQ, const handleAddressUpdate = (data) => {
projctNam: selectedProject.value.PROJCTNAM, addressData.value = data;
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.name,
originalColor: originalColor.value === selectedProject.value.PROJCTCOL ? null : originalColor.value
}).then(res => {
if (res.status === 200) {
toastStore.onToast('수정이 완료 되었습니다.', 's');
closeEditModal();
location.reload();
}
});
}; };
onMounted(async () => { //
await getProjectList(); watch([startDay, endDay], () => {
await userStore.userInfo(); if (startDay.value && endDay.value) {
user.value = userStore.user; const start = new Date(startDay.value);
}); const end = new Date(endDay.value);
//
if (end < start) {
endDay.value = startDay.value;
}
}
});
//
const handleCreate = async () => {
nameAlert.value = name.value.trim() === '';
addressAlert.value = addressData.value.address.trim() === '';
if (nameAlert.value || addressAlert.value) {
return;
}
$api.post('project/insert', {
projctNam: name.value,
projctCol: color.value,
projctStr: startDay.value,
projctEnd: endDay.value || null,
projctDes: description.value || null,
projctArr: addressData.value.address,
projctDtl: addressData.value.detailAddress,
projctZip: addressData.value.postcode,
projctCmb: user.value.name,
}).then(res => {
if (res.status === 200) {
toastStore.onToast('프로젝트가 등록되었습니다.', 's');
closeCreateModal();
getProjectList();
}
});
};
onMounted(async () => {
await getProjectList();
await userStore.userInfo();
user.value = userStore.user;
});
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="input-group mb-3 d-flex"> <div class="input-group mb-3 d-flex">
<input type="text" class="form-control" placeholder="Search" @change="search" /> <input type="text" class="form-control" placeholder="Search" @change="search" @input="preventLeadingSpace" />
<button type="button" class="btn btn-primary"><i class="bx bx-search bx-md"></i></button> <button type="button" class="btn btn-primary"><i class="bx bx-search bx-md"></i></button>
</div> </div>
</template> </template>
@ -15,11 +15,17 @@
}); });
const emits = defineEmits(['update:data']); const emits = defineEmits(['update:data']);
const search = function (event) { const search = function (event) {
//Type Number maxlength //Type Number maxlength
if (event.target.value.length > props.maxlength) { if (event.target.value.length > props.maxlength) {
event.target.value = event.target.value.slice(0, props.maxlength); event.target.value = event.target.value.slice(0, props.maxlength);
} }
emits('update:data', event.target.value); emits('update:data', event.target.value);
}; };
const preventLeadingSpace = function (event) {
event.target.value = event.target.value.trimStart();
}
</script> </script>

View File

@ -27,7 +27,7 @@
<strong class="mx-2 w-75">{{ item.WRDDICTTL }}</strong> <strong class="mx-2 w-75">{{ item.WRDDICTTL }}</strong>
</div> </div>
</div> </div>
<p class="mt-5" v-html="$common.contentToHtml(item.WRDDICCON)"></p> <p class="mt-5 dict-content-wrap" v-html="$common.contentToHtml(item.WRDDICCON)"></p>
<div class="d-flex justify-content-between flex-wrap gap-2 mb-2"> <div class="d-flex justify-content-between flex-wrap gap-2 mb-2">
<div class="d-flex flex-wrap align-items-center mb-50"> <div class="d-flex flex-wrap align-items-center mb-50">
<div class="avatar avatar-sm me-2"> <div class="avatar avatar-sm me-2">
@ -40,7 +40,7 @@
/> />
</div> </div>
<div> <div>
<p class="mb-0 small fw-medium">{{ formattedDate(item.author.createdAt) }}</p> <p class="mb-0 small fw-medium">{{ $common.dateFormatter(item.author.createdAt) }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -59,7 +59,7 @@
/> />
</div> </div>
<div> <div>
<p class="mb-0 small fw-medium">{{ formattedDate(item.lastEditor.updatedAt) }}</p> <p class="mb-0 small fw-medium">{{ $common.dateFormatter(item.lastEditor.updatedAt) }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -78,7 +78,6 @@ import { ref, toRefs, getCurrentInstance, } from 'vue';
import EditBtn from '@/components/button/EditBtn.vue'; import EditBtn from '@/components/button/EditBtn.vue';
import $api from '@api'; import $api from '@api';
import DictWrite from './DictWrite.vue'; import DictWrite from './DictWrite.vue';
import { formattedDate } from "@/common/formattedDate";
import { useUserInfoStore } from '@s/useUserInfoStore'; import { useUserInfoStore } from '@s/useUserInfoStore';
import { useWriteVisibleStore } from '@s/writeVisible'; import { useWriteVisibleStore } from '@s/writeVisible';

View File

@ -170,8 +170,10 @@ const saveWord = () => {
// //
let inserts = []; let inserts = [];
if(inserts.length === 0 && content.value?.ops?.length > 0){ if (inserts.length === 0 && content.value?.ops?.length > 0) {
inserts = content.value.ops.map(op => op.insert.trim()); inserts = content.value.ops.map(op =>
typeof op.insert === 'string' ? op.insert.trim() : op.insert
);
} }
// //

View File

@ -12,19 +12,16 @@ import $api from '@api';
export const useProjectStore = defineStore('project', () => { export const useProjectStore = defineStore('project', () => {
const projectList = ref([]); const projectList = ref([]);
const getProjectList = async (searchText, selectedYear) => { const getProjectList = async (searchText = '', selectedYear = '') => {
try { const res = await $api.get('project/select', {
const res = await $api.get('project/select', { params: {
params: { searchKeyword: searchText || '',
searchKeyword: searchText, category: selectedYear || '',
category: selectedYear, },
}, });
}); projectList.value = res.data.data.projectList;
projectList.value = res.data.data.projectList;
} catch (error) {
console.error('프로젝트 목록 조회 실패:', error);
}
}; };
return { projectList, getProjectList }; return { projectList, getProjectList };
}); });

View File

@ -536,5 +536,9 @@ watch([holidayDates, lastRemainingYear, lastRemainingMonth], () => {
</script> </script>
<style> <style>
/* 모달 본문 스크롤 */
.modal-body {
max-height: 130px;
overflow-y: auto;
}
</style> </style>

View File

@ -230,7 +230,7 @@
// isWriteVisible.value = false; // isWriteVisible.value = false;
writeStore.closeAll(); writeStore.closeAll();
getwordList(); getwordList();
const newCategory = { label: data, value: category }; // data const newCategory = { label: data, value: category };
cateList.value = [newCategory, ...cateList.value]; cateList.value = [newCategory, ...cateList.value];
} }
}); });
@ -244,7 +244,7 @@
} }
}); });
} }
}; };
// //