Merge branch 'main' into project-list
This commit is contained in:
commit
1b3339fa94
@ -68,7 +68,7 @@
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, ref } from 'vue';
|
||||
import BoardProfile from './BoardProfile.vue';
|
||||
import BoardCommentArea from './BoardComentArea.vue';
|
||||
import BoardCommentArea from './BoardCommentArea.vue';
|
||||
import PlusButton from '../button/PlusBtn.vue';
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<div class="d-flex justify-content-start align-items-top">
|
||||
<!-- 프로필섹션 -->
|
||||
<div class="avatar-wrapper">
|
||||
<div class="avatar me-4">
|
||||
<div v-if="!unknown" class="avatar me-4">
|
||||
<img src="/img/avatars/11.png" alt="Avatar" class="rounded-circle">
|
||||
</div>
|
||||
</div>
|
||||
@ -13,8 +13,9 @@
|
||||
<div class="w-100">
|
||||
<textarea
|
||||
class="form-control"
|
||||
placeholder="주제에 대한 생각을 자유롭게 댓글로 표현해 주세요. 여러분의 다양한 의견을 기다립니다."
|
||||
placeholder="댓글 달기"
|
||||
rows="3"
|
||||
v-model="comment"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@ -23,7 +24,7 @@
|
||||
<div class="d-flex justify-content-between flex-wrap mt-4">
|
||||
<div class="d-flex flex-wrap align-items-center">
|
||||
<!-- 익명 체크박스 (익명게시판일 경우에만)-->
|
||||
<div class="form-check form-check-inline mb-0 me-4">
|
||||
<div v-if="unknown" class="form-check form-check-inline mb-0 me-4">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
@ -40,15 +41,16 @@
|
||||
type="password"
|
||||
id="basic-default-password"
|
||||
class="form-control flex-grow-1"
|
||||
placeholder=""
|
||||
v-model="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 답변 쓰기 버튼 -->
|
||||
<div class="ms-auto mt-3 mt-md-0">
|
||||
<button class="btn btn-primary">
|
||||
<i class="icon-base bx bx-check"></i>
|
||||
<button class="btn btn-primary" @click="handleCommentSubmit">
|
||||
<!-- <i class="icon-base bx bx-check"></i> -->
|
||||
확인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -57,6 +59,40 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, defineEmits, defineProps, computed, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
unknown: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
parentId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
const comment = ref('');
|
||||
const password = ref('');
|
||||
const isCheck = ref(false);
|
||||
|
||||
const emit = defineEmits(['submitComment']);
|
||||
|
||||
watch(() => props.unknown, (newVal) => {
|
||||
if (!newVal) {
|
||||
isCheck.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
function handleCommentSubmit() {
|
||||
emit('submitComment', {
|
||||
comment: comment.value,
|
||||
password: password.value,
|
||||
});
|
||||
|
||||
comment.value = '';
|
||||
password.value = '';
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
@ -46,7 +46,6 @@
|
||||
</div>
|
||||
<!-- 에디터가 표시될 div -->
|
||||
<div ref="editor"></div>
|
||||
|
||||
<!-- Alert 메시지 표시 -->
|
||||
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">내용을 확인해주세요.</div>
|
||||
</div>
|
||||
@ -64,6 +63,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
initialData: {
|
||||
type: String,
|
||||
default: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
const editor = ref(null); // 에디터 DOM 참조
|
||||
@ -108,8 +111,14 @@ onMounted(() => {
|
||||
watch([font, fontSize], () => {
|
||||
quillInstance.format('font', font.value);
|
||||
quillInstance.format('size', fontSize.value);
|
||||
|
||||
});
|
||||
|
||||
// 초기 데이터가 있을 경우, HTML 형식으로 삽입
|
||||
if (props.initialData) {
|
||||
quillInstance.setContents(JSON.parse(props.initialData));
|
||||
}
|
||||
|
||||
// 이미지 업로드 기능 처리
|
||||
let imageUrls = new Set(); // 업로드된 이미지 URL을 추적
|
||||
quillInstance.getModule('toolbar').addHandler('image', () => {
|
||||
|
||||
@ -8,13 +8,13 @@
|
||||
<p>해당 직원에게 부여할 연차 개수를 선택하세요. (남은 개수: {{ availableQuota }}개)</p>
|
||||
|
||||
<div class="vacation-control">
|
||||
<button @click="decreaseCount" :disabled="grantCount <= 0" class="count-btn">-</button>
|
||||
<button @click="decreaseCount" :disabled="grantCount < 2" class="count-btn">-</button>
|
||||
<span class="grant-count">{{ grantCount }}</span>
|
||||
<button @click="increaseCount" :disabled="grantCount >= availableQuota" class="count-btn">+</button>
|
||||
</div>
|
||||
|
||||
<button class="gift-btn" @click="saveVacationGrant" :disabled="grantCount === 0">
|
||||
<i class="bx bx-gift"></i> <!-- 선물상자 아이콘 -->
|
||||
<i class="bx bx-gift"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,65 +26,60 @@
|
||||
import axios from "@api";
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: Boolean, // 모달 상태
|
||||
targetUser: Object, // 선택한 사용자 정보
|
||||
isOpen: Boolean,
|
||||
targetUser: Object,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "updateVacation"]);
|
||||
const grantCount = ref(0);
|
||||
const maxQuota = 2; // 1년에 보낼 수 있는 최대 개수
|
||||
const sentCount = ref(0); // 현재까지 보낸 개수
|
||||
const availableQuota = ref(2); // 남은 개수
|
||||
const maxQuota = 2;
|
||||
const sentCount = ref(0);
|
||||
const availableQuota = ref(2);
|
||||
|
||||
// ✅ 해당 사용자에게 이미 보낸 연차 개수 조회
|
||||
const fetchSentVacationCount = async () => {
|
||||
try {
|
||||
const payload = { receiverId: props.targetUser.MEMBERSEQ };
|
||||
|
||||
const response = await axios.get(`vacation/sent`,{ params: payload });
|
||||
console.log(response.data.data[0].count)
|
||||
sentCount.value = response.data.data[0].count || 0; // 이미 보낸 개수
|
||||
availableQuota.value = Math.max(maxQuota - sentCount.value, 0); // 남은 개수 (0 이하 방지)
|
||||
console.log(`✅ 보낸 개수: ${sentCount.value}, 남은 개수: ${availableQuota.value}`);
|
||||
const response = await axios.get("vacation/sent", { params: payload });
|
||||
sentCount.value = response.data.data[0].count || 0;
|
||||
availableQuota.value = Math.max(maxQuota - sentCount.value, 0);
|
||||
grantCount.value = availableQuota.value; // ✅ 남은 개수로 기본값 설정
|
||||
} catch (error) {
|
||||
console.error("🚨 연차 전송 기록 조회 실패:", error);
|
||||
availableQuota.value = maxQuota;
|
||||
grantCount.value = maxQuota; // 기본값 설정
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 연차 개수 증가
|
||||
const increaseCount = () => {
|
||||
if (grantCount.value < availableQuota.value) {
|
||||
grantCount.value++;
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 연차 개수 감소
|
||||
const decreaseCount = () => {
|
||||
if (grantCount.value > 0) {
|
||||
grantCount.value--;
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 연차 부여 저장 요청 (saveVacations API 호출)
|
||||
const saveVacationGrant = async () => {
|
||||
try {
|
||||
const payload = [
|
||||
{
|
||||
date: new Date().toISOString().split("T")[0], // 오늘 날짜
|
||||
type: "700103", // 연차 코드
|
||||
senderId: props.targetUser.senderId, // 보내는 사람 ID
|
||||
receiverId: props.targetUser.MEMBERSEQ, // 받는 사람 ID
|
||||
count: grantCount.value, // 부여 개수
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
type: "700103",
|
||||
senderId: props.targetUser.senderId,
|
||||
receiverId: props.targetUser.MEMBERSEQ,
|
||||
count: grantCount.value,
|
||||
},
|
||||
];
|
||||
|
||||
const response = await axios.post("vacation/save", payload);
|
||||
|
||||
const response = await axios.post("vacation", payload);
|
||||
console.log(response)
|
||||
if (response.data && response.data.status === "OK") {
|
||||
alert("✅ 연차가 부여되었습니다.");
|
||||
await fetchSentVacationCount(); // ✅ 보낸 개수 업데이트
|
||||
emit("updateVacation"); // ✅ 연차 정보 갱신 요청
|
||||
await fetchSentVacationCount();
|
||||
emit("updateVacation");
|
||||
closeModal();
|
||||
} else {
|
||||
alert("🚨 연차 추가 중 오류가 발생했습니다.");
|
||||
@ -95,36 +90,30 @@
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 모달 닫기
|
||||
const closeModal = () => {
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// ✅ 모달이 열릴 때 초기 값 설정 및 보낸 개수 조회
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
async (newVal) => {
|
||||
if (newVal && props.targetUser && props.targetUser.MEMBERSEQ) {
|
||||
console.log("🟢 모달이 열렸습니다. 데이터를 로드합니다.");
|
||||
grantCount.value = 0; // 초기화
|
||||
await fetchSentVacationCount(); // 보낸 개수 불러오기
|
||||
await fetchSentVacationCount();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ✅ targetUser가 변경될 때도 fetchSentVacationCount 호출
|
||||
watch(
|
||||
() => props.targetUser,
|
||||
async (newUser) => {
|
||||
if (newUser && newUser.MEMBERSEQ) {
|
||||
console.log(`🔄 새로운 대상(${newUser.name})이 선택되었습니다.`);
|
||||
await fetchSentVacationCount();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// ✅ 컴포넌트가 마운트될 때도 보낸 개수 불러오기
|
||||
onMounted(async () => {
|
||||
if (props.isOpen && props.targetUser && props.targetUser.MEMBERSEQ) {
|
||||
await fetchSentVacationCount();
|
||||
@ -132,6 +121,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
/* 모달 본문 */
|
||||
|
||||
@ -1,165 +1,186 @@
|
||||
<template>
|
||||
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal">
|
||||
<div class="modal-content modal-scroll">
|
||||
<h5 class="modal-title">📅 내 연차 사용 내역</h5>
|
||||
<button class="close-btn" @click="closeModal">✖</button>
|
||||
<div v-if="isOpen" class="modal-dialog" @click.self="closeModal">
|
||||
<div class="modal-content modal-scroll">
|
||||
<h5 class="modal-title">📅 내 연차 사용 내역</h5>
|
||||
<button class="close-btn" @click="closeModal">✖</button>
|
||||
|
||||
<!-- 연차 사용 및 받은 연차 리스트 -->
|
||||
<div class="modal-body" v-if="mergedVacations.length > 0">
|
||||
<ol class="vacation-list">
|
||||
<li
|
||||
v-for="(vacation, index) in mergedVacations"
|
||||
:key="index"
|
||||
class="vacation-item"
|
||||
>
|
||||
<span v-if="vacation.type === 'used'" class="vacation-index">
|
||||
{{ totalUsedVacationCount - usedVacations.findIndex(v => v.date === vacation.date) }})
|
||||
</span>
|
||||
<span :class="vacation.type === 'used' ? 'minus-symbol' : 'plus-symbol'">
|
||||
{{ vacation.type === 'used' ? '-' : '+' }}
|
||||
</span>
|
||||
<span :style="{ color: userColors[vacation.senderId || vacation.receiverId] || '#000' }"
|
||||
class="vacation-date">{{ formatDate(vacation.date) }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<!-- 연차 사용 및 받은 연차 리스트 -->
|
||||
<div class="modal-body" v-if="mergedVacations.length > 0">
|
||||
<ol class="vacation-list">
|
||||
<li v-for="(vacation, index) in mergedVacations" :key="index" class="vacation-item">
|
||||
<span v-if="vacation.type === 'used'" class="vacation-index">
|
||||
{{ getVacationIndex(index) }})
|
||||
</span>
|
||||
<span :class="vacation.type === 'used' ? 'minus-symbol' : 'plus-symbol'">
|
||||
{{ vacation.type === 'used' ? '-' : '+' }}
|
||||
</span>
|
||||
<span
|
||||
:style="{ color: userColors[vacation.senderId || vacation.receiverId] || '#000' }"
|
||||
class="vacation-date"
|
||||
>
|
||||
{{ formatDate(vacation.date) }}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- 연차 데이터 없음 -->
|
||||
<p v-if="mergedVacations.length === 0" class="no-data">
|
||||
🚫 사용한 연차가 없습니다.
|
||||
</p>
|
||||
<!-- 연차 데이터 없음 -->
|
||||
<p v-if="mergedVacations.length === 0" class="no-data">
|
||||
🚫 사용한 연차가 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: Boolean,
|
||||
myVacations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
receivedVacations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
userColors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isOpen: Boolean,
|
||||
myVacations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
receivedVacations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
userColors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close"]);
|
||||
|
||||
// ✅ 사용한 연차 개수
|
||||
const totalUsedVacationCount = computed(() => props.myVacations.length);
|
||||
|
||||
// ✅ 사용한 연차 + 받은 연차 통합 후 내림차순 정렬
|
||||
const usedVacations = computed(() => props.myVacations.map(v => ({ ...v, type: "used" })));
|
||||
const receivedVacations = computed(() => props.receivedVacations.map(v => ({ ...v, type: "received" })));
|
||||
|
||||
const mergedVacations = computed(() => {
|
||||
return [...usedVacations.value, ...receivedVacations.value].sort(
|
||||
(a, b) => new Date(b.date) - new Date(a.date)
|
||||
const usedVacations = computed(() =>
|
||||
props.myVacations.map(v => ({ ...v, type: "used" }))
|
||||
);
|
||||
|
||||
const receivedVacations = computed(() =>
|
||||
props.receivedVacations.map(v => ({ ...v, type: "received" }))
|
||||
);
|
||||
|
||||
// ✅ 정확한 정렬 및 리스트 병합
|
||||
const mergedVacations = computed(() => {
|
||||
return [...usedVacations.value, ...receivedVacations.value].sort(
|
||||
(a, b) => new Date(b.date) - new Date(a.date)
|
||||
);
|
||||
});
|
||||
|
||||
// ✅ 연차 개수 반영된 인덱스 반환 (누적 합산)
|
||||
const getVacationIndex = (index) => {
|
||||
let count = 0;
|
||||
for (let i = 0; i <= index; i++) {
|
||||
const v = mergedVacations.value[i];
|
||||
count += v.used_quota; // 누적하여 더함
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
// ✅ 날짜 형식 변환 (YYYY-MM-DD)
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split("T")[0]; // YYYY-MM-DD 형식
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split("T")[0]; // YYYY-MM-DD 형식
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
emit("close");
|
||||
emit("close");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 모달 스타일 */
|
||||
.modal-dialog {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 스크롤 가능한 모달 */
|
||||
.modal-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
width: 75%;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
width: 75%;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 닫기 버튼 */
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 리스트 기본 스타일 */
|
||||
.vacation-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin-top: 15px;
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/* 리스트 아이템 */
|
||||
.vacation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
background: #f9f9f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
/* 인덱스 (연차 사용 개수) */
|
||||
.vacation-index {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* "-" 빨간색 */
|
||||
.minus-symbol {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* "+" 파란색 */
|
||||
.plus-symbol {
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 날짜 스타일 */
|
||||
.vacation-date {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 연차 유형 스타일 */
|
||||
.vacation-type {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: gray;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* 연차 데이터 없음 */
|
||||
.no-data {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -66,51 +66,55 @@
|
||||
const props = defineProps({
|
||||
currentPage: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
pages: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
prePage: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
nextPage: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
isFirstPage: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
isLastPage: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
hasPreviousPage: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
hasNextPage: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
navigatePages: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
navigatepageNums: {
|
||||
type: Array,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
navigateFirstPage: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
navigateLastPage: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
PageData:{
|
||||
type:Array,
|
||||
required:false,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -2,32 +2,46 @@
|
||||
<div class="card mb-6">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-1"><div class="list-group-item list-group-item-action d-flex align-items-center cursor-pointer">
|
||||
<img src="/img/avatars/1.png" class="rounded-circle me-3 w-px-40" >
|
||||
<img
|
||||
class="rounded-circle user-avatar border border-3 w-px-40"
|
||||
:src="`${baseUrl}upload/img/profile/${data.localVote.MEMBERPRF}`"
|
||||
:style="`border-color: ${data.localVote.usercolor} !important;`"
|
||||
alt="user"
|
||||
/>
|
||||
<div class="w-100">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="user-info">
|
||||
<h6 class="mb-1">공공이</h6>
|
||||
<h6 class="mb-1">{{ data.localVote.MEMBERNAM }}</h6>
|
||||
</div>
|
||||
<div class="add-btn">
|
||||
<!-- 투표완료시 -->
|
||||
<i class="bx bxs-check-circle link-success"></i>
|
||||
<!-- 투표작성자만 수정/삭제/종료 가능 -->
|
||||
<button type="button" class="bx btn btn-danger">종료</button>
|
||||
<button
|
||||
v-if="userStore.user.id === data.localVote.LOCVOTREG"
|
||||
type="button" class="bx btn btn-danger">종료</button>
|
||||
<EditBtn />
|
||||
<DeleteBtn />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h5>
|
||||
<div class="mb-1">회식장소 고릅시다.</div>
|
||||
<div class="mb-1">24.12.12 11:02 ~ 24.12.12 16:02</div>
|
||||
<h5>{{ data.localVote.LOCVOTTTL }}</h5>
|
||||
<div class="mb-1">{{ data.localVote.formatted_LOCVOTRDT }} ~ {{ data.localVote.formatted_LOCVOTEDT }}</div>
|
||||
<!-- 투표완료시-->
|
||||
<vote-revote-end />
|
||||
<button v-if="data.yesVotetotal > 0" class="btn btn-primary btn-sm" >재투표</button>
|
||||
<!-- 투표안했을시-->
|
||||
<vote-card-check />
|
||||
<vote-card-check
|
||||
v-if="data.yesVotetotal == 0"
|
||||
@addContents="addContents"
|
||||
@checkedNames="checkedNames"
|
||||
:data="data.voteDetails"
|
||||
:voteInfo="data.localVote"
|
||||
:total="data.voteDetails.length "/>
|
||||
<!-- 투표완/미완 인원 -->
|
||||
<vote-user-list />
|
||||
<vote-user-list
|
||||
:data="data.voteMembers"/>
|
||||
<!-- 투표 결과 -->
|
||||
<vote-result-list />
|
||||
</div>
|
||||
@ -41,8 +55,25 @@ import DeleteBtn from '@c/button/DeleteBtn.vue';
|
||||
import voteUserList from '@c/voteboard/voteUserList.vue';
|
||||
import voteResultList from '@c/voteboard/voteResultList.vue';
|
||||
import voteCardCheck from '@c/voteboard/voteCardCheck.vue';
|
||||
import voteRevoteEnd from '@c/voteboard/voteRevoteEnd.vue';
|
||||
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||
import $api from '@api';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
|
||||
const userStore = useUserInfoStore();
|
||||
const emit = defineEmits(['addContents','checkedNames']);
|
||||
const addContents = (itemList, voteId) =>{
|
||||
emit('addContents',itemList,voteId)
|
||||
}
|
||||
const checkedNames = (numList) =>{
|
||||
emit('checkedNames',numList);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -2,24 +2,96 @@
|
||||
<div class="card-text">
|
||||
<div class="demo-inline-spacing mt-4">
|
||||
<!-- 투표리스트 -->
|
||||
<vote-card-check-list />
|
||||
<div class="d-flex align-items-center">
|
||||
<PlusBtn/>
|
||||
<FormInput title="추가항목" name="addContent" :isLabel="false" :is-essential="true" :is-alert="titleAlert" @update:data="title = $event" />
|
||||
<button class="btn btn-primary ms-1">저장</button>
|
||||
<div v-for="(item, index) in data"
|
||||
:key="index">
|
||||
<vote-card-check-list
|
||||
:data="item"
|
||||
:multiIs = voteInfo.LOCVOTMUL
|
||||
:selectedValues="checkedNames"
|
||||
@update:selectedValues="updateCheckedNames"
|
||||
/>
|
||||
<div v-if="voteInfo.LOCVOTADD ==='1' && index === data.length - 1" class="d-flex align-items-center">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-center">
|
||||
<form-input
|
||||
class="flex-grow-1 me-2"
|
||||
:title="'항목 ' + (index + data.length + 1)"
|
||||
:name="'content' + index"
|
||||
:is-essential="false"
|
||||
:is-alert="contentAlerts[index]"
|
||||
v-model="item.content"
|
||||
/>
|
||||
<link-input v-model="item.url" />
|
||||
<delete-btn @click="removeItem(index)" class="ms-2" />
|
||||
</div>
|
||||
<div class="mb-4 d-flex justify-content">
|
||||
<plus-btn @click="addItem" :disabled="total >= 10" class="mb-3" />
|
||||
<button class="btn btn-primary btn-icon mb-3" @click="addContentSave(item.LOCVOTSEQ)" :disabled="isSaveDisabled">
|
||||
<i class="bx bx-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm">투표하기</button>
|
||||
<button class="btn btn-primary btn-sm" @click="selectVote">투표하기</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import $api from '@api';
|
||||
import PlusBtn from '@c/button/PlusBtn.vue';
|
||||
import FormInput from '@c/input/FormInput.vue';
|
||||
import voteCardCheckList from '@c/voteboard/voteCardCheckList.vue';
|
||||
import { ref } from '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 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());
|
||||
});
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
voteInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['addContents','checkedNames']);
|
||||
//항목추가
|
||||
const addContentSave = (voteId) =>{
|
||||
emit('addContents',itemList.value,voteId);
|
||||
itemList.value = [{ content: "", url: "" }];
|
||||
}
|
||||
|
||||
const checkedNames = ref([]); // 선택된 값 저장
|
||||
const updateCheckedNames = (newValues) => {
|
||||
checkedNames.value = newValues;
|
||||
};
|
||||
const selectVote = () =>{
|
||||
emit('checkedNames',checkedNames.value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style >
|
||||
|
||||
@ -1,14 +1,57 @@
|
||||
<template>
|
||||
<div class="list-group">
|
||||
<label class="list-group-item">
|
||||
<input class="form-check-input me-1" type="checkbox" value="">
|
||||
case1
|
||||
<input
|
||||
class="form-check-input me-1"
|
||||
:name="data.LOCVOTSEQ"
|
||||
:type="multiIs === '1' ? 'checkbox' : 'radio'"
|
||||
:value="data.VOTDETSEQ"
|
||||
:checked="selectedValues.includes(data.VOTDETSEQ)"
|
||||
@change="handleChange"
|
||||
>
|
||||
{{ data.LOCVOTCON }}
|
||||
<a v-if="data.LOCVOTLIK" :href="data.LOCVOTLIK.startsWith('http') ? data.LOCVOTLIK : 'http://' + data.LOCVOTLIK" target="_blank">
|
||||
{{ data.LOCVOTLIK }}
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
multiIs: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
selectedValues: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(["update:selectedValues"]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const value = event.target.value;
|
||||
let updatedValues = [];
|
||||
|
||||
// 체크박스일 때 여러 개 선택 가능
|
||||
if (props.multiIs === "1") {
|
||||
updatedValues = event.target.checked
|
||||
? [...props.selectedValues, { VOTDETSEQ: value, LOCVOTSEQ: props.data.LOCVOTSEQ, LOCVOTCON: props.data.LOCVOTCON }]
|
||||
: props.selectedValues.filter(v => v.VOTDETSEQ !== value);
|
||||
} else {
|
||||
// 라디오 버튼일 때 하나만 선택 가능
|
||||
updatedValues = [{ VOTDETSEQ: value, LOCVOTSEQ: props.data.LOCVOTSEQ, LOCVOTCON: props.data.LOCVOTCON }];
|
||||
}
|
||||
emit("update:selectedValues", updatedValues);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@ -1,13 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<card />
|
||||
<card
|
||||
@addContents="addContents"
|
||||
@checkedNames="checkedNames"
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
:data="item" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import card from '@c/voteboard/voteCard.vue'
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['addContents','checkedNames']);
|
||||
const addContents = (itemList ,voteId) =>{
|
||||
emit('addContents',itemList ,voteId);
|
||||
}
|
||||
const checkedNames = (numList) =>{
|
||||
emit('checkedNames',numList);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class='bx bxs-user-check link-info'></i>
|
||||
<div class="d-flex align-items-center ">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap border-top-0 p-0">
|
||||
<div class="d-flex flex-wrap align-items-center">
|
||||
<ul class="list-unstyled users-list d-flex align-items-center avatar-group m-0 me-2">
|
||||
<vote-complete-user-list-card />
|
||||
<vote-complete-user-list-card :data="data"/>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@ -15,7 +14,12 @@
|
||||
|
||||
<script setup>
|
||||
import voteCompleteUserListCard from '@c/voteboard/voteCompleteUserListCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -1,10 +1,45 @@
|
||||
<template>
|
||||
<li data-bs-toggle="tooltip" data-popup="tooltip-custom" data-bs-placement="top" class="avatar pull-up" aria-label="Vinnie Mostowy" data-bs-original-title="Vinnie Mostowy">
|
||||
<img class="rounded-circle" src="/img/avatars/1.png" alt="Avatar">
|
||||
<li
|
||||
data-bs-toggle="tooltip"
|
||||
data-popup="tooltip-custom"
|
||||
data-bs-placement="top"
|
||||
class="avatar pull-up"
|
||||
:aria-label="data.MEMBERSEQ"
|
||||
:data-bs-original-title="getTooltipTitle(data)">
|
||||
<img
|
||||
class="rounded-circle user-avatar border border-3"
|
||||
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
|
||||
:style="`border-color: ${data.usercolor} !important;`"
|
||||
alt="user"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||
import $api from '@api';
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const userStore = useUserInfoStore();
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
|
||||
onMounted(async () => {
|
||||
nextTick(() => {
|
||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltips.forEach((tooltip) => {
|
||||
new bootstrap.Tooltip(tooltip);
|
||||
});
|
||||
});
|
||||
});
|
||||
const getTooltipTitle = (user) => {
|
||||
return user.MEMBERSEQ === userStore.user.id ? '나' : user.MEMBERNAM;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class='bx bxs-user-x link-danger'></i>
|
||||
<div class="d-flex align-items-center ">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap border-top-0 p-0">
|
||||
<div class="d-flex flex-wrap align-items-center">
|
||||
<ul class="list-unstyled users-list d-flex align-items-center avatar-group m-0 me-2">
|
||||
<vote-in-complete-user-list-card />
|
||||
<vote-in-complete-user-list-card :data="data" />
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@ -15,7 +14,12 @@
|
||||
|
||||
<script setup>
|
||||
import voteInCompleteUserListCard from '@c/voteboard/voteInCompleteUserListCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -1,10 +1,45 @@
|
||||
<template>
|
||||
<li data-bs-toggle="tooltip" data-popup="tooltip-custom" data-bs-placement="top" class="avatar pull-up" aria-label="Vinnie Mostowy" data-bs-original-title="Vinnie Mostowy">
|
||||
<img class="rounded-circle" src="/img/avatars/3.png" alt="Avatar">
|
||||
<li
|
||||
data-bs-toggle="tooltip"
|
||||
data-popup="tooltip-custom"
|
||||
data-bs-placement="top"
|
||||
class="avatar pull-up"
|
||||
:aria-label="data.MEMBERSEQ"
|
||||
:data-bs-original-title="getTooltipTitle(data)">
|
||||
<img
|
||||
class="rounded-circle user-avatar border border-3"
|
||||
:src="`${baseUrl}upload/img/profile/${data.MEMBERPRF}`"
|
||||
:style="`border-color: ${data.usercolor} !important;`"
|
||||
alt="user"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserInfoStore } from '@s/useUserInfoStore';
|
||||
import $api from '@api';
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const userStore = useUserInfoStore();
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
|
||||
onMounted(async () => {
|
||||
nextTick(() => {
|
||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltips.forEach((tooltip) => {
|
||||
new bootstrap.Tooltip(tooltip);
|
||||
});
|
||||
});
|
||||
});
|
||||
const getTooltipTitle = (user) => {
|
||||
return user.MEMBERSEQ === userStore.user.id ? '나' : user.MEMBERNAM;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
80
src/components/voteboard/voteLinkInput.vue
Normal file
80
src/components/voteboard/voteLinkInput.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="position-relative me-2">
|
||||
<i class="bx bx-link-alt" @click="togglePopover"></i>
|
||||
<!-- 링크 팝업 -->
|
||||
<div
|
||||
v-if="isPopoverVisible"
|
||||
class="popover bs-popover-auto fade show d-flex align-items-center"
|
||||
role="tooltip"
|
||||
:style="popoverStyle"
|
||||
>
|
||||
<div class="popover-arrow"></div>
|
||||
<input
|
||||
v-model="link"
|
||||
placeholder="URL을 입력해주세요"
|
||||
class="form-control me-2 flex-grow-1"
|
||||
style="min-width: 200px;"
|
||||
/>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-2" @click="saveLink">
|
||||
등록
|
||||
</button>
|
||||
</div>
|
||||
<!-- 등록된 링크 표시 -->
|
||||
<div v-if="link" class="mt-1">
|
||||
<a :href="formattedLink" target="_blank" rel="noopener noreferrer">{{ link }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: String,
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const isPopoverVisible = ref(false);
|
||||
const link = ref(props.modelValue || "");
|
||||
const popoverStyle = ref({});
|
||||
|
||||
const formattedLink = computed(() => {
|
||||
return link.value.startsWith("http") ? link.value : "http://" + link.value;
|
||||
});
|
||||
|
||||
const togglePopover = (event) => {
|
||||
const buttonRect = event.target.getBoundingClientRect();
|
||||
const parentRect = event.target.parentElement.getBoundingClientRect();
|
||||
|
||||
popoverStyle.value = {
|
||||
position: "absolute",
|
||||
top: `${buttonRect.bottom - parentRect.top + 5}px`,
|
||||
left: `${buttonRect.left - parentRect.left}px`,
|
||||
zIndex: "1050",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
};
|
||||
|
||||
isPopoverVisible.value = !isPopoverVisible.value;
|
||||
};
|
||||
|
||||
const saveLink = () => {
|
||||
emit("update:modelValue", link.value);
|
||||
isPopoverVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.popover {
|
||||
max-width: 300px;
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.popover-arrow {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<div class="user-status">
|
||||
<span class="badge badge-dot bg-warning"></span>
|
||||
<small>소고기 </small>
|
||||
<button class="btn btn-primary btn-sm">재투표</button>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@ -1,17 +1,42 @@
|
||||
<template>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<!--투표한 사람 목록 -->
|
||||
<vote-complete-user-list />
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class='bx bxs-user-check link-info'></i>
|
||||
<vote-complete-user-list
|
||||
v-for="(item, index) in voetedUsers"
|
||||
:key="index"
|
||||
:data="item"
|
||||
/>
|
||||
</div>
|
||||
<!-- 투표안한 사람 목록 -->
|
||||
<vote-in-complete-user-list />
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<i class='bx bxs-user-x link-danger'></i>
|
||||
<vote-in-complete-user-list
|
||||
v-for="(item, index) in noVoetedUsers"
|
||||
:key="index"
|
||||
:data="item"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import voteCompleteUserList from '@c/voteboard/voteCompleteUserList.vue';
|
||||
import voteInCompleteUserList from '@c/voteboard/voteInCompleteUserList.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const voetedUsers = computed(()=>{
|
||||
return props.data.filter(user => user.voted === 1);
|
||||
})
|
||||
const noVoetedUsers = computed(()=>{
|
||||
return props.data.filter(user => user.voted === 0);
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -4,10 +4,12 @@
|
||||
v-if="isWriteVisible"
|
||||
@close="isWriteVisible = false"
|
||||
:dataList="cateList"
|
||||
@addCategory="addCategory"
|
||||
@addCategory="addCategory"
|
||||
@addWord="editWord"
|
||||
:NumValue="item.WRDDICSEQ"
|
||||
:formValue="item.WRDDICCAT"
|
||||
:titleValue="item.WRDDICTTL"
|
||||
:contentValue="$common.contentToHtml(item.WRDDICCON)"
|
||||
:titleValue="item.WRDDICTTL"
|
||||
:contentValue="item.WRDDICCON"
|
||||
/>
|
||||
|
||||
<div v-else>
|
||||
@ -57,13 +59,16 @@
|
||||
<script setup>
|
||||
import axios from "@api";
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import { ref, toRefs } from 'vue';
|
||||
import { ref, toRefs, getCurrentInstance, } from 'vue';
|
||||
import EditBtn from '@/components/button/EditBtn.vue';
|
||||
import $api from '@api';
|
||||
import DictWrite from './DictWrite.vue';
|
||||
|
||||
const toastStore = useToastStore();
|
||||
|
||||
const { appContext } = getCurrentInstance();
|
||||
const $common = appContext.config.globalProperties.$common;
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
item: {
|
||||
@ -121,6 +126,42 @@ const addCategory = (data) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 용어집 수정
|
||||
const editWord = (data) => {
|
||||
console.log('📌 수정할 데이터:', data);
|
||||
console.log('📌 수정할 데이터:', data.id);
|
||||
console.log('📌 수정할 데이터:', data.category);
|
||||
console.log('📌 수정할 데이터:', data.title);
|
||||
console.log('📌 수정할 데이터:', $common.deltaAsJson(data.content));
|
||||
|
||||
if (!data.id) {
|
||||
console.error('❌ 수정할 데이터의 ID가 없습니다.');
|
||||
toastStore.onToast('수정할 용어의 ID가 필요합니다.', 'e');
|
||||
return;
|
||||
}
|
||||
|
||||
axios.patch('worddict/updateWord', {
|
||||
WRDDICSEQ: data.id,
|
||||
WRDDICCAT: 600104,
|
||||
WRDDICTTL: data.title,
|
||||
WRDDICRIK: $common.deltaAsJson(data.content),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data.data === '1') {
|
||||
toastStore.onToast('✅ 용어가 수정되었습니다.', 's');
|
||||
isWriteVisible.value = false; // 성공 시에만 닫기
|
||||
// getwordList(); // 목록 갱신
|
||||
} else {
|
||||
console.warn('⚠️ 서버 응답이 예상과 다릅니다:', res.data);
|
||||
toastStore.onToast('용어 수정이 정상적으로 처리되지 않았습니다.', 'e');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('❌ 용어 수정 중 오류 발생:', err.response?.data || err.message);
|
||||
toastStore.onToast(`용어 수정 실패: ${err.response?.data?.message || '알 수 없는 오류'}`, 'e');
|
||||
});
|
||||
};
|
||||
|
||||
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
||||
|
||||
// 날짜 포맷
|
||||
|
||||
@ -40,8 +40,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<QEditor @update:data="content = $event" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" />
|
||||
{{ contentValue }}
|
||||
<QEditor @update:data="content = $event" @update:imageUrls="imageUrls = $event" :is-alert="wordContentAlert" :initialData="contentValue"/>
|
||||
<div class="text-end mt-5">
|
||||
<button class="btn btn-primary" @click="saveWord">
|
||||
<i class="bx bx-check"></i>
|
||||
@ -79,6 +78,9 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
NumValue : {
|
||||
type: Number
|
||||
},
|
||||
formValue : {
|
||||
type:[String, Number]
|
||||
},
|
||||
@ -129,6 +131,7 @@ const saveWord = () => {
|
||||
}
|
||||
|
||||
const wordData = {
|
||||
id: props.NumValue || null,
|
||||
title: wordTitle.value,
|
||||
category: selectCategory.value,
|
||||
content: content.value,
|
||||
|
||||
26
src/stores/voteCommon.js
Normal file
26
src/stores/voteCommon.js
Normal file
@ -0,0 +1,26 @@
|
||||
// voteCommon.js
|
||||
import { ref } from "vue";
|
||||
|
||||
export function voteCommon(isVOte= false) {
|
||||
const itemList = ref(isVOte ? [] : [{ content: "", url: "" }, { content: "", url: "" }]);
|
||||
|
||||
const addItem = () => {
|
||||
if (itemList.value.length < 10) {
|
||||
itemList.value.push({ content: "", url: "" });
|
||||
}
|
||||
};
|
||||
|
||||
const removeItem = (index) => {
|
||||
if (!isVOte && index >= 2) {
|
||||
itemList.value.splice(index, 1);
|
||||
} else if (isVOte && itemList.value.length > 0) {
|
||||
itemList.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
itemList,
|
||||
addItem,
|
||||
removeItem,
|
||||
};
|
||||
}
|
||||
@ -190,6 +190,8 @@ const fetchGeneralPosts = async (page = 1) => {
|
||||
console.log(data)
|
||||
const totalPosts = data.data.total; // 전체 게시물 개수 받아오기
|
||||
|
||||
console.log('📌 API 응답 데이터:', data.data);
|
||||
|
||||
generalList.value = data.data.list.map((post, index) => ({
|
||||
realId: post.id,
|
||||
id: totalPosts - ((page - 1) * selectedSize.value) - index,
|
||||
|
||||
@ -94,7 +94,11 @@
|
||||
@updateReaction="handleUpdateReaction"
|
||||
@submitComment="handleCommentReply"
|
||||
/>
|
||||
<Pagination/>
|
||||
<Pagination
|
||||
v-if="pagination.pages"
|
||||
v-bind="pagination"
|
||||
@update:currentPage="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -103,7 +107,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BoardCommentArea from '@c/board/BoardComentArea.vue';
|
||||
import BoardCommentArea from '@/components/board/BoardCommentArea.vue';
|
||||
import BoardProfile from '@c/board/BoardProfile.vue';
|
||||
import BoardCommentList from '@c/board/BoardCommentList.vue';
|
||||
import BoardRecommendBtn from '@c/button/BoardRecommendBtn.vue';
|
||||
@ -141,6 +145,21 @@ const passwordAlert = ref("");
|
||||
const isPassword = ref(false);
|
||||
const lastClickedButton = ref("");
|
||||
|
||||
const pagination = ref({
|
||||
currentPage: 1,
|
||||
pages: 1,
|
||||
prePage: 0,
|
||||
nextPage: 1,
|
||||
isFirstPage: true,
|
||||
isLastPage: false,
|
||||
hasPreviousPage: false,
|
||||
hasNextPage: false,
|
||||
navigatePages: 10,
|
||||
navigatepageNums: [1],
|
||||
navigateFirstPage: 1,
|
||||
navigateLastPage: 1
|
||||
});
|
||||
|
||||
|
||||
// 게시물 상세 데이터 불러오기
|
||||
const fetchBoardDetails = async () => {
|
||||
@ -202,10 +221,13 @@ const handleUpdateReaction = async ({ boardId, commentId, isLike, isDislike }) =
|
||||
};
|
||||
|
||||
// 댓글 목록 조회
|
||||
const fetchComments = async () => {
|
||||
const fetchComments = async (page = 1) => {
|
||||
try {
|
||||
const response = await axios.get(`board/${currentBoardId.value}/comments`, {
|
||||
params: { LOCBRDSEQ: currentBoardId.value }
|
||||
params: {
|
||||
LOCBRDSEQ: currentBoardId.value,
|
||||
page
|
||||
}
|
||||
});
|
||||
console.log("목록 API 응답 데이터:", response.data);
|
||||
|
||||
@ -240,6 +262,22 @@ const fetchComments = async () => {
|
||||
|
||||
// console.log("변환된 comments 데이터:", comments.value);
|
||||
|
||||
pagination.value = {
|
||||
...pagination.value,
|
||||
currentPage: response.data.data.pageNum, // 현재 페이지 번호
|
||||
pages: response.data.data.pages, // 전체 페이지 수
|
||||
prePage: response.data.data.prePage, // 이전 페이지
|
||||
nextPage: response.data.data.nextPage, // 다음 페이지
|
||||
isFirstPage: response.data.data.isFirstPage, // 첫 페이지 여부
|
||||
isLastPage: response.data.data.isLastPage, // 마지막 페이지 여부
|
||||
hasPreviousPage: response.data.data.hasPreviousPage, // 이전 페이지 존재 여부
|
||||
hasNextPage: response.data.data.hasNextPage, // 다음 페이지 존재 여부
|
||||
navigatePages: response.data.data.navigatePages, // 몇 개의 페이지 버튼을 보여줄 것인지
|
||||
navigatepageNums: response.data.data.navigatepageNums, // 실제 페이지 번호 목록
|
||||
navigateFirstPage: response.data.data.navigateFirstPage, // 페이지네이션에서 첫 페이지 번호
|
||||
navigateLastPage: response.data.data.navigateLastPage // 페이지네이션에서 마지막 페이지 번호
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('댓글 목록 불러오기 오류:', error);
|
||||
}
|
||||
@ -372,6 +410,14 @@ const deletePost = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 페이지 변경
|
||||
const handlePageChange = (page) => {
|
||||
if (page !== pagination.value.currentPage) {
|
||||
pagination.value.currentPage = page;
|
||||
fetchComments(page);
|
||||
}
|
||||
};
|
||||
|
||||
// 날짜
|
||||
const formattedDate = (dateString) => {
|
||||
if (!dateString) return "날짜 없음";
|
||||
|
||||
@ -99,6 +99,7 @@ const handleProfileClick = async (user) => {
|
||||
if (user.MEMBERSEQ === userStore.user.id) {
|
||||
// 내 프로필을 클릭한 경우
|
||||
const response = await axios.get(`vacation/history`);
|
||||
console.log(response)
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
myVacations.value = response.data.data.usedVacations || [];
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
<div class="mt-8">
|
||||
<!-- 투표 작성 -->
|
||||
<router-link to="/voteboard/write"><WriteBtn /></router-link>
|
||||
|
||||
<!-- 내가한 투표 보기 -->
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="form-check me-3">
|
||||
@ -14,50 +13,87 @@
|
||||
<!-- 투표마감/투표중 셀렉트 -->
|
||||
<FormSelect class="col-3" name="cate" :isLabel="false" title="투표상태" :data="categoryList" @update:data="category = $event" />
|
||||
</div>
|
||||
|
||||
<!-- <QEditor @update:data="content = $event" @update:imageUrls="imageUrls = $event" :is-alert="true" />
|
||||
<button type="button" class="btn btn-primary ms-1" @click="registerContent"><i class="bx bx-check"></i></button> -->
|
||||
|
||||
<!-- 투표리스트 -->
|
||||
<vote-list />
|
||||
<vote-list
|
||||
:data="voteListCardData"
|
||||
@addContents="addContents"
|
||||
@checkedNames="checkedNames"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- <div class="mt-8">
|
||||
<pagination />
|
||||
</div> -->
|
||||
<!-- 페이지네이션 -->
|
||||
<div class="row g-3">
|
||||
<div class="mt-8">
|
||||
<Pagination
|
||||
v-if="PageData.pages"
|
||||
v-bind="PageData"
|
||||
@update:currentPage="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { getCurrentInstance, onMounted, ref } from 'vue';
|
||||
// import Pagination from '@/components/pagination/Pagination.vue';
|
||||
import Pagination from '@c/pagination/Pagination.vue';
|
||||
import router from '@/router';
|
||||
import FormSelect from '@c/input/FormSelect.vue';
|
||||
import { useToastStore } from '@s/toastStore';
|
||||
import QEditor from '@c/editor/QEditor.vue';
|
||||
import $api from '@api';
|
||||
import BoardCard from '@c/list/BoardCard.vue';
|
||||
import Quill from 'quill';
|
||||
import WriteBtn from '@c/button/WriteBtn.vue';
|
||||
import voteList from '@c/voteboard/voteCardList.vue';
|
||||
|
||||
const toastStore = useToastStore();
|
||||
const category = ref('0');
|
||||
const categoryList = ['전체','투표마감', '투표중'];
|
||||
const boardList = ref([]);
|
||||
const PageData = ref([]);
|
||||
const voteListCardData = ref([]);
|
||||
const titleAlert = ref(false);
|
||||
const addContent = ref('');
|
||||
|
||||
onMounted(()=>{
|
||||
getBoardList();
|
||||
})
|
||||
const getBoardList = () =>{
|
||||
$api.get('worddict/getWordList').then((res)=>{
|
||||
boardList.value = res.data.data.data;
|
||||
})
|
||||
const currentPage = ref(1);
|
||||
onMounted(async () => {
|
||||
getvoteList();
|
||||
});
|
||||
//투표목록
|
||||
const getvoteList = async () => {
|
||||
console.log('pagee',currentPage.value)
|
||||
const response = await $api.get('vote/getVoteList',{
|
||||
params: { page: currentPage.value }
|
||||
});
|
||||
if (response.data.status === "OK") {
|
||||
PageData.value = response.data.data;
|
||||
voteListCardData.value = response.data.data.list;
|
||||
}
|
||||
};
|
||||
//투표항목추가
|
||||
const addContents = (itemList, voteId) =>{
|
||||
$api.post('vote/insertWord',{
|
||||
itemList :itemList
|
||||
,voteId :voteId
|
||||
}).then((res)=>{
|
||||
if(res.data.status === 'OK'){
|
||||
toastStore.onToast('항목이 등록되었습니다.', 's');
|
||||
getvoteList();
|
||||
}
|
||||
})
|
||||
}
|
||||
//투표하기
|
||||
const checkedNames = (numList) =>{
|
||||
$api.post('vote/insertCheckedNums',{
|
||||
checkedList :numList
|
||||
,votenum : numList[0].LOCVOTSEQ
|
||||
}).then((res)=>{
|
||||
if(res.data.status === 'OK'){
|
||||
toastStore.onToast('투표가 완료되었습니다.', 's');
|
||||
getvoteList();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 페이지 변경
|
||||
const handlePageChange = async (page) => {
|
||||
currentPage.value=page;
|
||||
await getvoteList();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
@ -1,39 +1,194 @@
|
||||
<template>
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="card mb-6">
|
||||
<div class="card-body">
|
||||
<div class="user-list-container">
|
||||
<ul class="timeline mb-1">
|
||||
<li class="timeline-item timeline-item-transparent">
|
||||
<span class="timeline-point timeline-point-info"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-2">
|
||||
<h6 class="mb-0">투표 인원</h6>
|
||||
</div>
|
||||
<UserList @user-list-update="handleUserListUpdate" />
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="card mb-6">
|
||||
<div class="card-body">
|
||||
<div class="user-list-container">
|
||||
<ul class="timeline mb-1">
|
||||
<li class="timeline-item timeline-item-transparent">
|
||||
<span class="timeline-point timeline-point-info"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-2">
|
||||
<h6 class="mb-0">투표 인원</h6>
|
||||
</div>
|
||||
<UserList @userListInfo="userSet" @user-list-update="handleUserListUpdate" class="mb-3" />
|
||||
<div v-if="UserListAlert" class="red">2명이상 선택해주세요 </div>
|
||||
<form-input
|
||||
title="제목"
|
||||
name="title"
|
||||
:is-essential="true"
|
||||
:is-alert="titleAlert"
|
||||
v-model="title"
|
||||
/>
|
||||
<form-input
|
||||
title="종료날짜"
|
||||
name="endDate"
|
||||
type="date"
|
||||
:is-essential="true"
|
||||
:is-alert="endDateAlert"
|
||||
v-model="endDate"
|
||||
/>
|
||||
<!-- 항목 입력 반복 -->
|
||||
<div v-for="(item, index) in itemList" :key="index" class="d-flex align-items-center mb-2 position-relative">
|
||||
<form-input
|
||||
class="flex-grow-1 me-2"
|
||||
:title="'항목 ' + (index + 1)"
|
||||
:name="'content' + index"
|
||||
:is-essential="index < 2"
|
||||
:is-alert="contentAlerts[index]"
|
||||
v-model="item.content"
|
||||
/>
|
||||
<link-input v-model="item.url" />
|
||||
<delete-btn @click="removeItem(index)" :disabled="index < 2" class="ms-2" />
|
||||
</div>
|
||||
<plus-btn @click="addItem" :disabled="itemList.length >= 10" class="mb-3" />
|
||||
<div>
|
||||
<label class="list-group-item">
|
||||
<input
|
||||
class="form-check-input me-1"
|
||||
type="checkbox"
|
||||
id="addvoteitem"
|
||||
v-model="addvoteitem"
|
||||
/>
|
||||
항목 추가여부
|
||||
</label>
|
||||
<label class="list-group-item">
|
||||
<input
|
||||
class="form-check-input me-1"
|
||||
type="checkbox"
|
||||
id="addvotemulti"
|
||||
v-model="addvotemulti"
|
||||
/>
|
||||
다중투표 허용여부
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 d-flex justify-content-end">
|
||||
<button type="button" class="btn btn-info" @click="goList">
|
||||
<i class="bx bx-left-arrow-alt"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary ms-1" @click="saveValid">
|
||||
<i class="bx bx-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ref, toRaw } from "vue";
|
||||
import UserList from "@c/user/UserList.vue";
|
||||
import formInput from "@c/input/FormInput.vue";
|
||||
import { useToastStore } from "@s/toastStore";
|
||||
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';
|
||||
|
||||
const activeUsers = ref([]); // 활성화된 사용자 목록
|
||||
const disabledUsers = ref([]); // 비활성화된 사용자 목록
|
||||
const toastStore = useToastStore();
|
||||
const activeUserList = ref([]);
|
||||
const disabledUsers = ref([]);
|
||||
const titleAlert = ref(false);
|
||||
const endDateAlert = ref(false);
|
||||
const contentAlerts = ref([false, false]);
|
||||
const UserListAlert = ref(false);
|
||||
const title = ref("");
|
||||
const endDate = ref("");
|
||||
const { itemList, addItem, removeItem } = voteCommon();
|
||||
|
||||
// UserList에서 받은 데이터를 처리
|
||||
const handleUserListUpdate = ({ activeUsers, disabledUsers }) => {
|
||||
activeUsers.value = activeUsers;
|
||||
disabledUsers.value = disabledUsers;
|
||||
console.log('활성화목록>>',activeUsers)
|
||||
console.log('비활성목록>>',disabledUsers)
|
||||
const userListTotal = ref(0);
|
||||
const addvoteitem = ref(false);
|
||||
const addvotemulti= ref(false);
|
||||
|
||||
const userSet = ({ userList, userTotal }) => {
|
||||
activeUserList.value = userList;
|
||||
userListTotal.value = userTotal;
|
||||
};
|
||||
|
||||
const handleUserListUpdate = ({ activeUsers, disabledUsers: updatedDisabledUsers }) => {
|
||||
activeUserList.value = activeUsers;
|
||||
disabledUsers.value = updatedDisabledUsers;
|
||||
userListTotal.value = activeUsers.length;
|
||||
};
|
||||
|
||||
const saveValid = () => {
|
||||
let valid = true;
|
||||
|
||||
if (title.value === '') {
|
||||
titleAlert.value = true;
|
||||
valid = false;
|
||||
} else {
|
||||
titleAlert.value = false;
|
||||
}
|
||||
|
||||
if (endDate.value === '') {
|
||||
endDateAlert.value = true;
|
||||
valid = false;
|
||||
} else {
|
||||
endDateAlert.value = false;
|
||||
}
|
||||
|
||||
if (itemList.value[0].content === '') {
|
||||
contentAlerts.value[0] = true;
|
||||
valid = false;
|
||||
} else {
|
||||
contentAlerts.value[0] = false;
|
||||
}
|
||||
|
||||
if (itemList.value[1].content === '') {
|
||||
contentAlerts.value[1] = true;
|
||||
valid = false;
|
||||
} else {
|
||||
contentAlerts.value[1] = false;
|
||||
}
|
||||
|
||||
if (activeUserList.value.length < 2) {
|
||||
UserListAlert.value = true;
|
||||
valid = false;
|
||||
} else {
|
||||
UserListAlert.value = false;
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
saveVote();
|
||||
}
|
||||
};
|
||||
|
||||
const saveVote = () => {
|
||||
console.log('activeUserList',activeUserList.value)
|
||||
const unwrappedUserList = toRaw(activeUserList.value);
|
||||
const listId = unwrappedUserList.map(item => ({
|
||||
id: item.MEMBERSEQ,
|
||||
}));
|
||||
$api.post('vote/insertWord',{
|
||||
addvoteIs :addvoteitem ? '1' :'0'
|
||||
,votemMltiIs :addvotemulti ? '1' :'0'
|
||||
,title :title.value
|
||||
,endDate :endDate.value
|
||||
,itemList :itemList.value
|
||||
,activeUserList :listId
|
||||
}).then((res)=>{
|
||||
if(res.data.status == 'OK'){
|
||||
toastStore.onToast('투표가 등록되었습니다.', 's');
|
||||
goList();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const goList = () => {
|
||||
router.push('/voteboard');
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item-input {
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -176,7 +176,6 @@
|
||||
}
|
||||
//용어 등록
|
||||
const addWord = (wordData) => {
|
||||
|
||||
axios.post('worddict/insertWord',{
|
||||
WRDDICCAT : wordData.category,
|
||||
WRDDICTTL : wordData.title,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user