Merge branch 'main' of http://192.168.0.251:3000/localnet/localhost-front
All checks were successful
LocalNet_front/pipeline/head This commit looks good

This commit is contained in:
yoon 2025-03-21 19:56:01 +09:00
commit aa22023ca3
21 changed files with 509 additions and 176 deletions

View File

@ -7,7 +7,6 @@
/* board */
.board-content img {
max-width: 100% !important;
width: 100% !important;
height: auto !important;
display: block;
object-fit: contain;
@ -520,6 +519,24 @@
/* project list */
.hidden-start-input {
position: absolute;
top: 103%;
left: 20%;
width: 100%;
height: 40px;
opacity: 0;
}
.hidden-end-input {
position: absolute;
top: 113.3%;
left: 20%;
width: 100%;
height: 40px;
opacity: 0;
}
.map {
top: -160px;
left: -5px;
@ -684,6 +701,16 @@
/* BoardComment end */
/* vote */
.hidden-date-input {
position: absolute;
top: 31.5%;
left: 17%;
width: 100%;
opacity: 0;
pointer-events: none;
}
/* 권한부여 */
.user-card-container {
display: flex;

View File

@ -1,23 +1,10 @@
/* @font-face {
font-family: 'GmarketSansMedium';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2001@1.1/GmarketSansMedium.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'GmarketSansMedium', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 1) */
/* @font-face {
font-family: 'NanumSquareRound';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_two@1.0/NanumSquareRound.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'NanumSquareRound', sans-serif;
font-size: 16px;
@ -25,13 +12,13 @@ body {
color: #333;
} */
/* 2) */
/* @font-face {
font-family: 'Pretendard-Regular';
src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}
body {
font-family: 'Pretendard-Regular', sans-serif;
font-size: 16px;
@ -39,20 +26,117 @@ body {
color: #333;
} */
/* 3) */
/* @font-face {
font-family: 'S-CoreDream-3Light';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_six@1.2/S-CoreDream-3Light.woff') format('woff');
font-family: 'NEXON Lv1 Gothic OTF';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_20-04@2.1/NEXON Lv1 Gothic OTF.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'S-CoreDream-3Light', sans-serif;
font-family: 'NEXON Lv1 Gothic OTF', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 4) */
/* @font-face {
font-family: 'SUITE-Regular';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2304-2@1.0/SUITE-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
body {
font-family: 'SUITE-Regular', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 5) */
/* @font-face {
font-family: 'GoyangIlsan';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_one@1.0/GoyangIlsan.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'GoyangIlsan', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 6) */
/* @font-face {
font-family: 'GowunDodum-Regular';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'GowunDodum-Regular', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 7) */
/* @font-face {
font-family: 'EASTARJET-Medium';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_231029@1.1/EASTARJET-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
}
body {
font-family: 'EASTARJET-Medium', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 8) */
/* @font-face {
font-family: 'HakgyoansimChulseokbuTTF-B';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/2408-5@1.0/HakgyoansimChulseokbuTTF-B.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
body {
font-family: 'HakgyoansimChulseokbuTTF-B', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 9) */
/* @font-face {
font-family: 'GongGothicMedium';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_20-10@1.0/GongGothicMedium.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'GongGothicMedium', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* @font-face {
font-family: 'MangoDdobak-B';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/2405-3@1.1/MangoDdobak-B.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
body {
font-family: 'MangoDdobak-B', sans-serif;
font-size: 16px;
line-height: 1.6;
color: #333;
} */
/* 나눔고딕 */
@import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic&display=swap');

View File

@ -4,7 +4,7 @@
:unknown="comment.author === '익명'"
:isCommentAuthor="isCommentAuthor"
:boardId="comment.boardId"
:profileName="comment.nickname ? comment.nickname : comment.author"
:profileName="displayName"
:date="comment.createdAt"
:comment="comment"
:profileImg="comment.profileImg"
@ -76,10 +76,15 @@
type: Boolean,
default: false,
},
nickname: {
type: String,
default: '',
},
isCommentAuthor: {
type: Boolean,
default: false,
},
isPlusButton: {
type: Boolean,
default: true,
@ -113,6 +118,10 @@
editCommentAlert: String,
});
const displayName = computed(() => {
return props.nickname? props.nickname: props.comment.author;
});
// emits
const emit = defineEmits([
'submitComment',

View File

@ -7,6 +7,7 @@
:isCommentAuthor="comment.isCommentAuthor"
:isEditTextarea="comment.isEditTextarea"
:isDeleted="isDeleted"
:nickname="comment.nickname"
:isCommentPassword="isCommentPassword"
:passwordCommentAlert="passwordCommentAlert || ''"
:currentPasswordCommentId="currentPasswordCommentId"
@ -32,6 +33,7 @@
:isPlusButton="false"
:isLike="true"
:isCommentProfile="true"
:nickname="child.nickname"
:isCommentAuthor="child.isCommentAuthor"
:isCommentPassword="isCommentPassword"
:currentPasswordCommentId="currentPasswordCommentId"

View File

@ -12,7 +12,7 @@
</div>
<div class="me-2">
<h6 class="mb-0">{{ profileName }}</h6>
<h6 class="mb-0">{{ profileName ? profileName : nickname }}</h6>
<div class="profile-detail">
<span>{{ date }}</span>
<template v-if="showDetail">
@ -62,6 +62,10 @@
type: String,
default: '',
},
nickname: {
type: String,
default: '',
},
unknown: {
type: Boolean,
default: true,

View File

@ -6,7 +6,7 @@
<button class="vac-btn vac-btn-warning rounded-circle d-flex align-items-center justify-content-center"
:class="{ active: halfDayType === 'AM' }"
@click="toggleHalfDay('AM')">
<i class="bi bi-sun"></i>
<i class="bi bi-sun d-flex"></i>
</button>
</div>
</div>
@ -16,7 +16,7 @@
<button class="vac-btn vac-btn-info rounded-circle d-flex align-items-center justify-content-center"
:class="{ active: halfDayType === 'PM' }"
@click="toggleHalfDay('PM')">
<i class="bi bi-moon"></i>
<i class="bi bi-moon d-flex"></i>
</button>
</div>
</div>
@ -26,7 +26,7 @@
<button class="vac-btn vac-btn-primary rounded-circle d-flex align-items-center justify-content-center"
:class="{ active: halfDayType === 'FULL' }"
@click="toggleHalfDay('FULL')">
<i class="bi bi-calendar"></i>
<i class="bi bi-calendar d-flex"></i>
</button>
</div>
</div>

View File

@ -71,7 +71,8 @@
const editor = ref(null); // DOM
const font = ref('nanum-gothic'); //
const fontSize = ref('16px'); //
const emit = defineEmits(['update:data']);
const emit = defineEmits(['update:data', 'update:uploadedImgList']);
const uploadedImgList = ref([]); //
onMounted(() => {
//
@ -112,6 +113,13 @@
quillInstance.format('size', fontSize.value);
});
//
watch(uploadedImgList, () => {
console.log(!23);
emit('update:uploadedImgList', uploadedImgList.value);
console.log('uploadedImgList.value: ', uploadedImgList.value);
});
// , HTML
if (props.initialData) {
quillInstance.setContents(JSON.parse(props.initialData));
@ -150,7 +158,15 @@
// URL
uploadImageToServer(formData)
.then(serverImageUrl => {
.then(data => {
const uploadImgIdx = data?.fileIndex; // DB
const serverImageUrl = data?.fileUrl; // url
// ( )
if (uploadImgIdx) {
uploadedImgList.value = [...uploadedImgList.value, uploadImgIdx];
}
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const fullImageUrl = `${baseUrl}${serverImageUrl.replace(/\\/g, '/')}`;
@ -201,7 +217,7 @@
// Throw the error so the caller knows something went wrong
throw error;
}
}
}
//
function checkForDeletedImages() {
@ -216,4 +232,3 @@
}
});
</script>

View File

@ -19,6 +19,9 @@
@focusout="$emit('focusout', modelValue)"
@input="handleInput"
/>
<div v-if="isBtn" class="ms-2">
<slot name="append"></slot>
</div>
</div>
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div>
<div class="invalid-feedback" :class="isCateAlert ? 'd-block' : ''">카테고리 중복입니다.</div>

View File

@ -5,7 +5,7 @@
<span :class="isEssential ? 'link-danger' : 'd-none'">*</span>
</label>
<div class="col-md-12">
<div v-if="useInputGroup" class="input-group mb-3">
<div v-if="useInputGroup" class="input-group mb-1">
<input
:id="name"
class="form-control"
@ -14,7 +14,6 @@
:value="computedValue"
:disabled="disabled"
:maxLength="maxlength"
:minLength="minlength"
:placeholder="title"
@blur="$emit('blur')"
/>
@ -30,7 +29,6 @@
:value="computedValue"
:disabled="disabled"
:maxLength="maxlength"
:minLength="minlength"
:placeholder="title"
@blur="$emit('blur')"
/>

View File

@ -130,6 +130,8 @@
</div>
</div>
<!-- 시작일 -->
<div class="date-picker-wrapper" @click="focusStartDateInput">
<FormInput
title="시작일"
type="date"
@ -138,7 +140,11 @@
:modelValue="selectedProject.PROJCTSTR"
@update:modelValue="selectedProject.PROJCTSTR = $event"
/>
<input ref="startDateInput" type="date" v-model="selectedProject.PROJCTSTR" class="hidden-start-input">
</div>
<!-- 종료일 -->
<div class="date-picker-wrapper" @click="focusEndDateInput">
<FormInput
title="종료일"
type="date"
@ -147,6 +153,8 @@
:modelValue="selectedProject.PROJCTEND"
@update:modelValue="selectedProject.PROJCTEND = $event"
/>
<input ref="endDateInput" type="date" v-model="selectedProject.PROJCTEND" class="hidden-end-input">
</div>
<FormInput
title="설명"
@ -199,6 +207,31 @@ const toastStore = useToastStore();
const userStore = useUserInfoStore();
const projectStore = useProjectStore();
const startDateInput = ref(null);
const endDateInput = ref(null);
const focusStartDateInput = () => {
nextTick(() => {
if (startDateInput.value) {
startDateInput.value.focus();
setTimeout(() => {
startDateInput.value.showPicker?.();
}, 50);
}
});
};
const focusEndDateInput = () => {
nextTick(() => {
if (endDateInput.value) {
endDateInput.value.focus();
setTimeout(() => {
endDateInput.value.showPicker?.();
}, 50);
}
});
};
// Props
const props = defineProps({
title: {
@ -548,3 +581,13 @@ onMounted(async () => {
</script>
<style>
.hidden-date-input {
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: 40px;
opacity: 0;
}
</style>

View File

@ -69,6 +69,7 @@
</div>
</div>
<div class="date-picker-wrapper" @click="focusStartDateInput">
<FormInput
title="시작 일"
name="startDay"
@ -77,15 +78,20 @@
:modelValue="startDay"
v-model="startDay"
/>
<input ref="startDateInput" type="date" v-model="startDay" class="hidden-start-input">
</div>
<div class="date-picker-wrapper" @click="focusEndDateInput">
<FormInput
title="종료 일"
:type="'date'"
name="endDay"
:type="'date'"
:modelValue="endDay"
:min = "startDay"
:min="startDay"
@update:modelValue="endDay = $event"
/>
<input ref="endDateInput" type="date" v-model="endDay" class="hidden-end-input">
</div>
<FormInput
title="설명"
@ -161,6 +167,31 @@
const nameAlert = ref(false);
const addressAlert = ref(false);
const startDateInput = ref(null);
const endDateInput = ref(null);
const focusStartDateInput = () => {
nextTick(() => {
if (startDateInput.value) {
startDateInput.value.focus();
setTimeout(() => {
startDateInput.value.showPicker?.();
}, 50);
}
});
};
const focusEndDateInput = () => {
nextTick(() => {
if (endDateInput.value) {
endDateInput.value.focus();
setTimeout(() => {
endDateInput.value.showPicker?.();
}, 50);
}
});
};
const addressData = ref({
postcode: '',
address: '',

View File

@ -28,7 +28,6 @@
</button>
<DeleteBtn v-if="!data.localVote.LOCVOTDDT" @click="voteDelete(data.localVote.LOCVOTSEQ)" />
</div>
<p v-if="data.localVote.LOCVOTDDT" class="btn-icon btn-danger rounded-2 pe-none"><i class="bx bx-power-off"></i></p>
</div>
</div>
</div>
@ -53,7 +52,7 @@
<small v-if="yesVotetotal != 0 && !data.localVote.LOCVOTDDT">투표 완료 : 종료시 투표 결과가 나타납니다.</small>
<!-- 투표 결과 -->
<div v-if="data.localVote.LOCVOTDDT" class="mt-3">
<vote-result-list :data="topVoters" @randomList="randomList" :randomResultNum="data.localVote.LOCVOTRES" :locvotreg="data.localVote.LOCVOTREG"/>
<vote-result-list :data="topVoters" @randomList="randomList" :randomResultNum="data.localVote.LOCVOTRES" :locvotreg="data.localVote.LOCVOTREG" />
</div>
<!-- 투표완/미완 인원 -->
<vote-user-list
@ -68,7 +67,7 @@
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import EditBtn from '@c/button/EditBtn.vue';
import DeleteBtn from '@c/button/DeleteBtn.vue';
import voteUserList from '@c/voteboard/voteUserList.vue';
@ -105,9 +104,10 @@ const topVoters = computed(() => {
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const userStore = useUserInfoStore();
const currentDate = new Date();
const voteEndDate = new Date(props.data.localVote.formatted_LOCVOTEDT.replace(' ', 'T'));
voteEndDate.setDate(voteEndDate.getDate() + 1);
const offset = new Date().getTimezoneOffset() * 60000
const today = new Date(Date.now() - offset);
const currentDate = today.toISOString().substring(0,16);
const voteEndDate = props.data.localVote.LOCVOTEDT.substring(0,16);
//
const isVoteEnded = computed(() => {
return currentDate > voteEndDate;
@ -117,7 +117,19 @@ onMounted(() => {
if (isVoteEnded.value && !props.data.localVote.LOCVOTDDT) {
emit('voteEnded', { id: props.data.localVote.LOCVOTSEQ });
}
checkVoteCompletion();
});
//
watch(() => props.data.localVote.total_voted, () => {
checkVoteCompletion();
});
//
const checkVoteCompletion = () => {
if (props.data.localVote.total_votable === props.data.localVote.total_voted && props.data.localVote.LOCVOTDDT == '') {
emit('voteEnded', { id: props.data.localVote.LOCVOTSEQ });
}
};
const addContents = (itemList, voteId) =>{
emit('addContents',itemList,voteId)
}
@ -125,7 +137,6 @@ const checkedNames = (numList) =>{
emit('checkedNames',numList);
}
const endBtn = (voteid) =>{
voteEndDate.setTime(currentDate.getTime()); //
emit('endVoteId',voteid);
}
const voteDelete = (voteid) =>{

View File

@ -17,16 +17,23 @@
:is-alert="contentAlerts[index]"
v-model="item.content"
:is-btn="true"
@keyup="ValidHandler('content' + (index + 1))"
>
<template v-slot:append>
<delete-btn @click="removeItem(index)" />
</template>
</form-input>
<link-input v-model="item.url" class="mb-1"/>
<form-input
:title="'URL ' + (index + data.length + 1)"
:name="'url' + index"
v-model="item.url"
:is-essential="false"
class="mb-1"
/>
</div>
<div class="d-flex justify-content align-items-center mt-3">
<plus-btn @click="addItem" :disabled="total >= 10" />
<button class="btn btn-primary btn-icon m-1" @click="addContentSave(item.LOCVOTSEQ)" :disabled="isSaveDisabled">
<plus-btn @click="addItem" :disabled=" total >= 10" />
<button class="btn btn-primary btn-icon m-1" @click="addContentSave(item.LOCVOTSEQ ,index)" :disabled="isSaveDisabled">
<i class="bx bx-check"></i>
</button>
</div>
@ -46,20 +53,19 @@ import SaveBtn from '@c/button/SaveBtn.vue'
import FormInput from '@c/input/FormInput.vue';
import voteCardCheckList from '@c/voteboard/voteCardCheckList.vue';
import { computed, ref } from 'vue';
import LinkInput from "@/components/voteboard/voteLinkInput.vue";
import { voteCommon } from '@s/voteCommon';
import DeleteBtn from "@c/button/DeleteBtn.vue";
import { useToastStore } from '@s/toastStore';
import router from '@/router';
const toastStore = useToastStore();
const contentAlerts = ref(false);
const contentAlerts = ref([false, false]);
const titleAlert = ref(false);
const title = ref('');
const rink = ref('');
const { itemList, addItem, removeItem } = voteCommon(true);
const total = computed(() => props.total + itemList.value.length);
const isSaveDisabled = computed(() => {
return itemList.value.length === 0 || itemList.value.every(item => !item.content.trim());
return itemList.value.length === 0 || itemList.value.every(item => !item.content.trim() && !item.url.trim());
});
const props = defineProps({
data: {
@ -77,12 +83,35 @@ const props = defineProps({
});
const emit = defineEmits(['addContents','checkedNames']);
//
const addContentSave = (voteId) =>{
const addContentSave = (voteId,index) =>{
let valid = true;
const filteredItemList = itemList.value.filter(item => item.content && item.content.trim() !== '');
itemList.value.forEach((item, index) => {
if (!item.content.trim() && item.url.trim()) {
contentAlerts.value[index] = true;
valid = false;
} else {
contentAlerts.value[index] = false;
}
});
if(valid){
emit('addContents',filteredItemList,voteId);
itemList.value = [{ content: "", url: "" }];
removeItem();
}
}
const ValidHandler = (field) => {
if (field.startsWith('content')) {
const index = parseInt(field.replace('content', '')) - 1;
if (!isNaN(index)) {
contentAlerts.value[index] = false;
}
}
};
const checkedNames = ref([]); //
const updateCheckedNames = (newValues) => {
checkedNames.value = newValues;

View File

@ -57,6 +57,7 @@
v-if="contentLoaded"
@update:data="content = $event"
@update:imageUrls="imageUrls = $event"
@update:uploadedImgList="handleUpdateEditorImg"
:initialData="content"
/>
</div>
@ -115,7 +116,7 @@
const attachFilesAlert = ref(false);
const isFileValid = ref(true);
const delFileIdx = ref([]); // ID
const additionalFiles = ref([]); //
const editorUploadedImgList = ref([]);
//
const fetchBoardDetails = async () => {
@ -145,6 +146,10 @@
contentLoaded.value = true;
};
const handleUpdateEditorImg = item => {
editorUploadedImgList.value = item;
};
//
const addDisplayFileName = fileInfos =>
fileInfos.map(file => ({
@ -247,6 +252,11 @@
boardData.delFileIdx = [...delFileIdx.value];
}
//
if (editorUploadedImgList.value && editorUploadedImgList.value.length > 0) {
boardData.editorUploadedImgList = [...editorUploadedImgList.value];
}
const fileArray = newFileFilter(attachFiles);
const formData = new FormData();

View File

@ -101,7 +101,7 @@
<span v-if="isNewPost(post.rawDate)" class="box-new badge text-white ms-2 fs-tiny">N</span>
</div>
</td>
<td class="text-center">{{ post.author }}</td>
<td class="text-center">{{ post.nickname ? post.nickname : post.author }}</td>
<td class="text-center">{{ post.date }}</td>
<td class="text-center">{{ post.views }}</td>
</tr>
@ -219,6 +219,7 @@
date: formatDate(post.date), //
views: post.cnt || 0,
hasAttachment: post.hasAttachment,
nickname: post.nickname || null,
img: post.firstImageUrl || null,
commentCount: post.commentCount,
}));

View File

@ -8,10 +8,11 @@
<div class="pb-5 border-bottom">
<BoardProfile
:boardId="currentBoardId"
:profileName="profileName"
:profileName="displayName"
:unknown="unknown"
:profileImg="profileImg"
:views="views"
:nickname="nickname"
:commentNum="commentNum"
:date="formattedBoardDate"
:isLike="false"
@ -151,6 +152,7 @@
const profileName = ref('');
const boardTitle = ref('제목 없음');
const boardContent = ref('');
const nickname = ref('');
const date = ref('');
const views = ref(0);
const likes = ref(0);
@ -174,6 +176,10 @@
const authorId = ref(''); // id
const editCommentAlert = ref({}); //,
const displayName = computed(() => {
return nickname.value && unknown.value ? nickname.value : profileName.value;
});
const isAuthor = computed(() => currentUserId.value === authorId.value);
const commentsWithAuthStatus = computed(() => {
const updatedComments = comments.value.map(comment => ({
@ -257,6 +263,7 @@
boardContent.value = boardData.content || '';
profileImg.value = boardData.profileImg || '';
date.value = boardData.date || '';
nickname.value = boardData.nickname || '';
views.value = boardData.cnt || 0;
likes.value = boardData.likeCount || 0;
dislikes.value = boardData.dislikeCount || 0;
@ -322,7 +329,7 @@
likeCount: comment.likeCount || 0,
dislikeCount: comment.dislikeCount || 0,
profileImg: comment.profileImg || '',
nickname: comment.LOCCMTNIC,
nickname: comment.LOCCMTNIC || '',
likeClicked: comment.likeClicked || false,
dislikeClicked: comment.dislikeClicked || false,
createdAtRaw: comment.LOCCMTRDT, //
@ -352,7 +359,7 @@
parentId: reply.LOCCMTPNT, // ID
content: reply.LOCCMTRPY || '내용 없음',
createdAtRaw: reply.LOCCMTRDT,
nickname: reply.LOCCMTNIC,
nickname: reply.LOCCMTNIC || '',
// createdAt: formattedDate(reply.LOCCMTRDT),
//createdAtRaw: new Date(reply.LOCCMTUDT),
createdAt: formattedDate(reply.LOCCMTUDT) + (reply.LOCCMTUDT !== reply.LOCCMTRDT ? ' (수정됨)' : ''),
@ -435,7 +442,7 @@
LOCCMTRPY: reply.comment,
LOCCMTPWD: reply.password || null,
LOCCMTPNT: reply.parentId,
LOCCMTNIC: data.isCheck ? data.nickname : null,
LOCCMTNIC: reply.isCheck ? reply.nickname : null,
LOCBRDTYP: reply.isCheck ? '300102' : null,
});

View File

@ -42,6 +42,15 @@
<!-- 비밀번호 필드 (익명게시판 선택 활성화) -->
<div v-if="categoryValue === 300102" class="mb-4">
<FormInput
title="닉네임"
name="nickname"
:is-essential="true"
:is-alert="nicknameAlert"
v-model="nickname"
@update:alert="nicknameAlert = $event"
@input="validateNickname"
/>
<FormInput
title="비밀번호"
name="pw"
@ -83,7 +92,7 @@
<div class="mb-4">
<label class="col-md-2 col-form-label"> 내용 <span class="text-danger">*</span> </label>
<div class="col-md-12">
<QEditor @update:data="content = $event" />
<QEditor @update:data="content = $event" @update:uploadedImgList="handleUpdateEditorImg" />
</div>
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">내용을 입력해주세요.</div>
</div>
@ -112,12 +121,14 @@
const toastStore = useToastStore();
const categoryList = ref([]);
const title = ref('');
const nickname = ref("");
const password = ref('');
const categoryValue = ref(null);
const content = ref({ ops: [] });
const isFileValid = ref(true);
const titleAlert = ref(false);
const nicknameAlert = ref(false);
const passwordAlert = ref(false);
const contentAlert = ref(false);
const categoryAlert = ref(false);
@ -127,6 +138,7 @@
const maxFiles = 5;
const maxSize = 10 * 1024 * 1024;
const fileError = ref('');
const editorUploadedImgList = ref([]);
const fetchCategories = async () => {
const response = await axios.get('board/categories');
@ -143,6 +155,10 @@
const fileCount = computed(() => attachFiles.value.length);
const handleUpdateEditorImg = item => {
editorUploadedImgList.value = item;
};
const handleFileUpload = files => {
const validFiles = files.filter(file => file.size <= maxSize);
if (files.some(file => file.size > maxSize)) {
@ -172,6 +188,14 @@
titleAlert.value = title.value.trim().length === 0;
};
const validateNickname = () => {
if (categoryValue.value === 300102) {
nicknameAlert.value = nickname.value.trim().length === 0;
} else {
nicknameAlert.value = false;
}
};
const validatePassword = () => {
if (categoryValue.value === 300102) {
password.value = password.value.replace(/\s/g, ''); //
@ -199,11 +223,12 @@
/** 글쓰기 */
const write = async () => {
validateTitle();
validateNickname();
validatePassword();
validateContent();
categoryAlert.value = categoryValue.value == null;
if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
if (titleAlert.value || nicknameAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
return;
}
@ -211,10 +236,16 @@
const boardData = {
LOCBRDTTL: title.value,
LOCBRDCON: JSON.stringify(content.value), // Delta JSON
LOCBRDNIC: categoryValue.value === 300102 ? nickname.value : null,
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
LOCBRDTYP: categoryValue.value,
};
//
if (editorUploadedImgList.value && editorUploadedImgList.value.length > 0) {
boardData.editorUploadedImgList = [...editorUploadedImgList.value];
}
const { data: boardResponse } = await axios.post('board', boardData);
const boardId = boardResponse.data;
// ( )

View File

@ -180,7 +180,6 @@ function handleDateClick(info) {
//
if (currentValue && currentValue !== "delete") {
console.log("🛑 활성화된 날짜 비활성화:", clickedDateStr);
selectedDates.value.delete(clickedDateStr);
updateCalendarEvents();
return;
@ -203,7 +202,6 @@ function handleDateClick(info) {
// -
if (isMyVacation) {
console.log("🗑 기존 휴가 삭제 후 새로운 상태 추가:", clickedDateStr);
selectedDates.value.set(clickedDateStr, "delete");
}
@ -647,7 +645,7 @@ onMounted(async () => {
const dpEl = calendarDatepicker.value;
dpEl.style.display = 'block';
dpEl.style.position = 'fixed';
dpEl.style.top = '25%';
dpEl.style.top = '18%';
dpEl.style.left = '50%';
dpEl.style.transform = 'translate(-50%, -50%)';
dpEl.style.zIndex = '9999';

View File

@ -132,17 +132,18 @@ const checkedNames = (numList) => {
}
//
const endVoteId = (endVoteId) => {
console.log('endVoteId',endVoteId)
$api.patch('vote/updateEndData',{
endVoteId :endVoteId
}).then((res)=>{
if(res.data.status === 'OK'){
toastStore.onToast('투표가 종료되었습니다.', 's');
getvoteList();
}
})
}
//
const voteEnded = async (id) =>{
console.log('voteEnded',id)
await endVoteId(id.id);
}
//

View File

@ -20,16 +20,22 @@
v-model="title"
@keyup="ValidHandler('title')"
/>
<div class="date-picker-container" @click="focusDateInput">
<form-input
title="종료날짜"
name="endDate"
type="date"
type="datetime-local"
:is-essential="true"
:is-alert="endDateAlert"
v-model="endDate"
:min="today"
@change="ValidHandlerendDate"
:min="minDate"
@input="ValidHandlerendDate"
/>
</div>
<!-- 숨겨진 input 태그를 사용하여 강제로 포커스 -->
<input ref="dateInput" type="datetime-local" v-model="endDate" class="hidden-date-input">
<!-- 항목 입력 반복 -->
<div v-for="(item, index) in itemList" :key="index">
<form-input
@ -45,8 +51,14 @@
<delete-btn @click="removeItem(index)" :disabled="index < 2" />
</template>
</form-input>
<link-input v-model="item.url" class="mb-1"/>
<form-input
:title="'URL ' + (index + 1)"
:name="'url' + index"
v-model="item.url"
:is-essential="false"
class="mb-1"
/>
<!-- <link-input v-model="item.url" class="mb-1"/> -->
</div>
<plus-btn @click="addItem" :disabled="itemList.length >= 10" class="mb-3" />
@ -90,7 +102,7 @@
</template>
<script setup>
import { onMounted, ref, toRaw } from "vue";
import { nextTick, onMounted, ref, toRaw } from "vue";
import UserList from "@c/user/UserList.vue";
import formInput from "@c/input/FormInput.vue";
import { useToastStore } from "@s/toastStore";
@ -98,13 +110,15 @@ import PlusBtn from "@c/button/PlusBtn.vue";
import DeleteBtn from "@c/button/DeleteBtn.vue";
import $api from "@api";
import router from "@/router";
import LinkInput from "@/components/voteboard/voteLinkInput.vue";
import { voteCommon } from '@s/voteCommon';
import { useUserStore } from '@s/userList';
import { useRoute } from "vue-router";
const userStore = useUserStore();
const today = new Date().toISOString().substring(0, 10);
// const offset = new Date().getTimezoneOffset() * 60000
// const today = new Date(Date.now() - offset);
// today.setDate(today.getDate() + 1);
// const minDate = today.toISOString().substring(0, 16);
const toastStore = useToastStore();
const activeUserList = ref([]);
const disabledUsers = ref([]);
@ -119,12 +133,29 @@ const userListTotal = ref(0);
const addvoteitem = ref(false);
const addvotemulti= ref(false);
const dateInput = ref(null);
const focusDateInput = () => {
if (dateInput.value) {
dateInput.value.showPicker(); // ( )
dateInput.value.focus(); //
}
};
const minDate = ref('');
onMounted(() => {
nextTick(() => {
const offset = new Date().getTimezoneOffset() * 60000;
const today = new Date(Date.now() - offset);
today.setDate(today.getDate() + 1);
minDate.value = today.toISOString().substring(0, 16);
});
});
const userSet = ({ userList, userTotal }) => {
activeUserList.value = userList;
userListTotal.value = userTotal;
};
const handleUserListUpdate = ({ activeUsers, disabledUsers: updatedDisabledUsers }) => {
activeUserList.value = activeUsers;
disabledUsers.value = updatedDisabledUsers;
@ -135,7 +166,6 @@ const handleUserListUpdate = ({ activeUsers, disabledUsers: updatedDisabledUsers
UserListAlert.value = false;
}
};
const saveValid = () => {
let valid = true;
if (disabledUsers.value.length === 0) {
@ -175,7 +205,6 @@ const saveValid = () => {
}
};
const saveVote = () => {
console.log('itemList',itemList)
const filteredItemList = itemList.value.filter(item => item.content && item.content.trim() !== '');
const unwrappedUserList = toRaw(activeUserList.value);
const listId = unwrappedUserList.map(item => ({