Merge branch 'main' into wordDict
This commit is contained in:
commit
11d936bcc5
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "front",
|
"name": "front",
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "front",
|
"name": "front",
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fullcalendar/core": "^6.1.15",
|
"@fullcalendar/core": "^6.1.15",
|
||||||
"@fullcalendar/daygrid": "^6.1.15",
|
"@fullcalendar/daygrid": "^6.1.15",
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
<component :is="layout">
|
<component :is="layout">
|
||||||
<template #content>
|
<template #content>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
<ToastModal />
|
|
||||||
</template>
|
</template>
|
||||||
</component>
|
</component>
|
||||||
|
<ToastModal />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|||||||
49
src/common/common.js
Normal file
49
src/common/common.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
작성자 : 공현지
|
||||||
|
작성일 : 2025-01-17
|
||||||
|
수정자 :
|
||||||
|
수정일 :
|
||||||
|
설명 : 공통 스크립트
|
||||||
|
*/
|
||||||
|
import Quill from 'quill';
|
||||||
|
|
||||||
|
/*
|
||||||
|
*템플릿 사용법 : $common.변수
|
||||||
|
*setup() 사용법 :
|
||||||
|
const { appContext } = getCurrentInstance();
|
||||||
|
const $common = appContext.config.globalProperties.$common;
|
||||||
|
$common.변수
|
||||||
|
*/
|
||||||
|
const common = {
|
||||||
|
// JSON 문자열로 Delta 타입을 변환
|
||||||
|
contentToHtml(content) {
|
||||||
|
try {
|
||||||
|
if (content.startsWith('{') || content.startsWith('[')) {
|
||||||
|
// Delta 형식으로 변환
|
||||||
|
const delta = JSON.parse(content);
|
||||||
|
const quill = new Quill(document.createElement('div'));
|
||||||
|
quill.setContents(delta);
|
||||||
|
return quill.root.innerHTML; // HTML 반환
|
||||||
|
}
|
||||||
|
return content; // 이미 HTML일 경우 그대로 반환
|
||||||
|
} catch (error) {
|
||||||
|
console.error('콘텐츠 변환 오류:', error);
|
||||||
|
return content; // 오류 발생 시 원본 반환
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Delta 타입을 JSON 문자열로 변환
|
||||||
|
deltaAsJson(content) {
|
||||||
|
if (content && content.ops) {
|
||||||
|
return JSON.stringify(content.ops); // Delta 객체에서 ops 속성만 JSON 문자열로 변환
|
||||||
|
}
|
||||||
|
console.error('잘못된 Delta 객체:', content);
|
||||||
|
return null; // Delta 객체가 아니거나 ops가 없을 경우 null 반환
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(app) {
|
||||||
|
app.config.globalProperties.$common = common;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -46,20 +46,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 에디터가 표시될 div -->
|
<!-- 에디터가 표시될 div -->
|
||||||
<div ref="editor"></div>
|
<div ref="editor"></div>
|
||||||
|
|
||||||
|
<!-- Alert 메시지 표시 -->
|
||||||
|
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">내용을 확인해주세요.</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Quill from 'quill';
|
import Quill from 'quill';
|
||||||
import 'quill/dist/quill.snow.css';
|
import 'quill/dist/quill.snow.css';
|
||||||
import { onMounted, ref, watch, defineEmits } from 'vue';
|
import { onMounted, ref, watch, defineEmits, defineProps } from 'vue';
|
||||||
import $api from '@api';
|
import $api from '@api';
|
||||||
|
const props = defineProps({
|
||||||
|
isAlert: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
const editor = ref(null);
|
const editor = ref(null);
|
||||||
const font = ref('nanum-gothic');
|
const font = ref('nanum-gothic');
|
||||||
const fontSize = ref('16px');
|
const fontSize = ref('16px');
|
||||||
const emit = defineEmits(['update:data']);
|
const emit = defineEmits(['update:data']);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const Font = Quill.import('formats/font');
|
const Font = Quill.import('formats/font');
|
||||||
Font.whitelist = ['nanum-gothic', 'd2coding', 'consolas', 'serif', 'monospace'];
|
Font.whitelist = ['nanum-gothic', 'd2coding', 'consolas', 'serif', 'monospace'];
|
||||||
@ -83,21 +90,23 @@ onMounted(() => {
|
|||||||
quillInstance.format('size', fontSize.value);
|
quillInstance.format('size', fontSize.value);
|
||||||
|
|
||||||
quillInstance.on('text-change', () => {
|
quillInstance.on('text-change', () => {
|
||||||
emit('update:data', quillInstance.root.innerHTML);
|
const delta = quillInstance.getContents(); // Get Delta format
|
||||||
|
emit('update:data', delta);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch([font, fontSize], () => {
|
watch([font, fontSize], () => {
|
||||||
quillInstance.format('font', font.value);
|
quillInstance.format('font', font.value);
|
||||||
quillInstance.format('size', fontSize.value);
|
quillInstance.format('size', fontSize.value);
|
||||||
});
|
});
|
||||||
// 이미지 업로드
|
|
||||||
let imageUrls = new Set();
|
|
||||||
|
|
||||||
|
// Handle image upload
|
||||||
|
let imageUrls = new Set();
|
||||||
quillInstance.getModule('toolbar').addHandler('image', () => {
|
quillInstance.getModule('toolbar').addHandler('image', () => {
|
||||||
selectLocalImage();
|
selectLocalImage();
|
||||||
});
|
});
|
||||||
quillInstance.on('text-change', (delta, oldDelta, source) => {
|
quillInstance.on('text-change', (delta, oldDelta, source) => {
|
||||||
emit('update:data', quillInstance.root.innerHTML);
|
// Emit Delta when content changes
|
||||||
|
emit('update:data', quillInstance.getContents());
|
||||||
delta.ops.forEach(op => {
|
delta.ops.forEach(op => {
|
||||||
if (op.insert && typeof op.insert === 'object' && op.insert.image) {
|
if (op.insert && typeof op.insert === 'object' && op.insert.image) {
|
||||||
const imageUrl = op.insert.image;
|
const imageUrl = op.insert.image;
|
||||||
@ -107,13 +116,11 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function selectLocalImage() {
|
async function selectLocalImage() {
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.setAttribute('type', 'file');
|
input.setAttribute('type', 'file');
|
||||||
input.setAttribute('accept', 'image/*');
|
input.setAttribute('accept', 'image/*');
|
||||||
input.click();
|
input.click();
|
||||||
|
|
||||||
input.onchange = () => {
|
input.onchange = () => {
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
@ -136,7 +143,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
async function uploadImageToServer(formData) {
|
async function uploadImageToServer(formData) {
|
||||||
try {
|
try {
|
||||||
const response = await $api.post('img/upload', formData, { isFormData: true });
|
const response = await $api.post('quilleditor/upload', formData, { isFormData: true });
|
||||||
const imageUrl = response.data.data;
|
const imageUrl = response.data.data;
|
||||||
return imageUrl;
|
return imageUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -144,7 +151,6 @@ onMounted(() => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForDeletedImages() {
|
function checkForDeletedImages() {
|
||||||
const editorImages = document.querySelectorAll('#editor img');
|
const editorImages = document.querySelectorAll('#editor img');
|
||||||
const currentImages = new Set(Array.from(editorImages).map(img => img.src));
|
const currentImages = new Set(Array.from(editorImages).map(img => img.src));
|
||||||
@ -156,12 +162,9 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@import 'quill/dist/quill.snow.css';
|
@import 'quill/dist/quill.snow.css';
|
||||||
|
|
||||||
.ql-editor {
|
.ql-editor {
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
font-family: 'Nanum Gothic', sans-serif;
|
font-family: 'Nanum Gothic', sans-serif;
|
||||||
|
|||||||
@ -114,7 +114,6 @@ const emitAddressData = () => {
|
|||||||
postcode: postcode.value,
|
postcode: postcode.value,
|
||||||
address: address.value,
|
address: address.value,
|
||||||
detailAddress: detailAddress.value,
|
detailAddress: detailAddress.value,
|
||||||
fullAddress: `${address.value} ${detailAddress.value}`.trim()
|
|
||||||
};
|
};
|
||||||
emits('update:data', fullAddress);
|
emits('update:data', fullAddress);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,17 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-4 row">
|
<div class="mb-4 row">
|
||||||
<label :for="name" class="col-md-2 col-form-label">{{ title }} </label>
|
<label :for="name" class="col-md-2 col-form-label">{{ title }}</label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<input class="form-control" type="file" :id="name" @change="changeHandler" multiple />
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="file"
|
||||||
|
:id="name"
|
||||||
|
@change="changeHandler"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
<div v-if="showError" class="text-danger mt-1">
|
||||||
|
{{ errorMsg }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback" :class="isAlert ? 'display-block' : ''">{{ errorMsg }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { fileMsg } from '@/common/msgEnum';
|
import { ref, watch } from 'vue';
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
|
// Props
|
||||||
const prop = defineProps({
|
const prop = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -23,24 +31,34 @@ const prop = defineProps({
|
|||||||
default: 'nameplz',
|
default: 'nameplz',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
isAlert : {
|
isAlert: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['update:data']);
|
// Emits
|
||||||
const errorMsg = ref(fileMsg.FileMaxSizeMsg);
|
const emits = defineEmits(['update:data', 'update:isValid']);
|
||||||
|
|
||||||
//파일 검사 하는거 만들어야겠지...
|
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB
|
||||||
|
const errorMsg = ref('첨부파일의 총 용량이 5MB를 초과합니다.');
|
||||||
|
const showError = ref(false);
|
||||||
|
|
||||||
|
// Change Handler
|
||||||
const changeHandler = (event) => {
|
const changeHandler = (event) => {
|
||||||
const files = Array.from(event.target.files);
|
const files = Array.from(event.target.files);
|
||||||
emits('update:data', files);
|
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
||||||
|
|
||||||
|
if (totalSize > MAX_TOTAL_SIZE) {
|
||||||
|
showError.value = true; // 에러 메시지 표시
|
||||||
|
emits('update:data', []); // 부모 컴포넌트로 빈 배열 전달
|
||||||
|
emits('update:isValid', false); // 유효하지 않은 상태 전달
|
||||||
|
} else {
|
||||||
|
showError.value = false; // 에러 메시지 숨기기
|
||||||
|
emits('update:data', files); // 부모 컴포넌트로 파일 전달
|
||||||
|
emits('update:isValid', true); // 유효한 상태 전달
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-2 row">
|
<div class="mb-2 row">
|
||||||
<label :for="name" class="col-md-2 col-form-label">
|
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<span :class="isEssential ? 'text-red' : 'none'">*</span>
|
<span v-if="isEssential" class="text-danger">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<input
|
<input
|
||||||
@ -55,6 +55,11 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isLabel : {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emits 정의
|
// Emits 정의
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mb-2" :class="isRow ?'row' : ''">
|
<div class="mb-2" :class="isRow ? 'row' : ''">
|
||||||
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
|
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<span :class="isEssential ? 'link-danger' : 'none'">*</span>
|
<span :class="isEssential ? 'link-danger' : 'none'">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div :class="isRow ?'col-md-10' : 'col-md-12'">
|
<div :class="isRow ? 'col-md-10' : 'col-md-12'">
|
||||||
<select class="form-select" :id="name" v-model="selectData">
|
<select class="form-select" :id="name" v-model="selectData">
|
||||||
<option v-for="(item , i) in data" :key="item" :value="i" :selected="value == i">{{ item }}</option>
|
<option v-for="(item, i) in data" :key="i" :value="isCommon ? item.value : i">
|
||||||
|
{{ isCommon ? item.label : item }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isAlert" class="invalid-feedback">{{ title }}을 확인해주세요.</div>
|
<div v-if="isAlert" class="invalid-feedback">{{ title }}을 확인해주세요.</div>
|
||||||
@ -50,19 +52,30 @@ const props = defineProps({
|
|||||||
isLabel : {
|
isLabel : {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
required: true,
|
required: false,
|
||||||
},
|
},
|
||||||
isRow : {
|
isRow : {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
required: true,
|
required: false,
|
||||||
|
},
|
||||||
|
isCommon : {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:data']);
|
const emit = defineEmits(['update:data']);
|
||||||
const selectData = ref(props.value);
|
const selectData = ref(props.value);
|
||||||
|
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
|
if (props.isCommon && props.data.length > 0) {
|
||||||
|
selectData.value = props.data[0].value; // 첫 번째 옵션의 값으로 설정
|
||||||
|
} else {
|
||||||
|
selectData.value = props.value; // 기본값으로 설정
|
||||||
|
}
|
||||||
emit('update:data', selectData.value);
|
emit('update:data', selectData.value);
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -11,9 +11,10 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
:type="type"
|
:type="type"
|
||||||
@input="updateInput"
|
@input="updateInput"
|
||||||
:value="value"
|
:value="computedValue"
|
||||||
:maxLength="maxlength"
|
:maxLength="maxlength"
|
||||||
:placeholder="title"
|
:placeholder="title"
|
||||||
|
@blur="$emit('blur')"
|
||||||
/>
|
/>
|
||||||
<span class="input-group-text">@ localhost.co.kr</span>
|
<span class="input-group-text">@ localhost.co.kr</span>
|
||||||
</div>
|
</div>
|
||||||
@ -23,21 +24,21 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
:type="type"
|
:type="type"
|
||||||
@input="updateInput"
|
@input="updateInput"
|
||||||
:value="value"
|
:value="computedValue"
|
||||||
:maxLength="maxlength"
|
:maxLength="maxlength"
|
||||||
:placeholder="title"
|
:placeholder="title"
|
||||||
|
@blur="$emit('blur')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }}를 확인해주세요.</div>
|
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }}를 확인해주세요.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { inject, computed } from 'vue';
|
||||||
|
|
||||||
const prop = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '라벨',
|
default: '라벨',
|
||||||
@ -61,14 +62,14 @@ const prop = defineProps({
|
|||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
require: false,
|
required: false,
|
||||||
},
|
},
|
||||||
maxlength: {
|
maxlength: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 30,
|
default: 30,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
isAlert : {
|
isAlert: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
@ -78,23 +79,35 @@ const prop = defineProps({
|
|||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['update:data', 'update:alert'])
|
const emits = defineEmits(['update:data', 'update:alert', 'blur']);
|
||||||
|
|
||||||
const updateInput = function (event) {
|
// dayjs 인스턴스 가져오기
|
||||||
//Type Number 일때 maxlength 적용 안됨 방지
|
const dayjs = inject('dayjs');
|
||||||
if (event.target.value.length > prop.maxlength) {
|
|
||||||
event.target.value = event.target.value.slice(0, prop.maxlength);
|
// 오늘 날짜를 YYYY-MM-DD 형식으로 변환
|
||||||
|
const today = dayjs().format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// date인 경우 기본값 -> 오늘 날짜
|
||||||
|
const computedValue = computed(() => {
|
||||||
|
return props.type === 'date' ? props.value || today : props.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 입력값 업데이트
|
||||||
|
const updateInput = event => {
|
||||||
|
const newValue = event.target.value.slice(0, props.maxlength);
|
||||||
|
|
||||||
|
// date인 경우 날짜 형식 유지
|
||||||
|
if (props.type === 'date') {
|
||||||
|
emits('update:data', newValue.replace(/[^0-9-]/g, ''));
|
||||||
|
} else {
|
||||||
|
emits('update:data', newValue);
|
||||||
}
|
}
|
||||||
emits('update:data', event.target.value);
|
|
||||||
|
|
||||||
// 값이 입력될 때 isAlert를 false로 설정
|
// 값이 입력될 때 isAlert를 false로 설정
|
||||||
if (event.target.value.trim() !== '') { emits('update:alert', false); }
|
if (newValue.trim() !== '') {
|
||||||
|
emits('update:alert', false);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -11,26 +11,34 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 게시물 내용 섹션 -->
|
<!-- 게시물 내용 섹션 -->
|
||||||
<div class="col-md-10">
|
<div :class="contentColClass">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- 태그 -->
|
<!-- 태그 -->
|
||||||
<h6 class="badge rounded-pill bg-primary text-white mb-2">
|
<h6 class="badge rounded-pill bg-primary text-white mb-2">
|
||||||
{{ category }}
|
{{ category }}
|
||||||
</h6>
|
</h6>
|
||||||
<!-- 제목 -->
|
<!-- 제목 -->
|
||||||
<h5 class="card-title">{{ title }}</h5>
|
<h5 class="card-title">
|
||||||
|
{{ title }}
|
||||||
|
<span class="text-muted me-3" v-if="attachment">
|
||||||
|
<i class="fa-solid fa-paperclip"></i>
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
<!-- 본문 -->
|
<!-- 본문 -->
|
||||||
<p class="card-text str_wrap">{{ content }}</p>
|
<div class="card-text str_wrap" v-html="$common.contentToHtml(content)"></div>
|
||||||
<!-- 날짜 -->
|
<!-- 날짜 -->
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<small class="text-muted">{{ formatDate(date) }}</small>
|
<small class="text-muted">{{ formattedDate }}</small>
|
||||||
<!-- 좋아요와 댓글 -->
|
<!-- 조회수, 좋아요, 댓글 -->
|
||||||
<div>
|
<div>
|
||||||
<span class="text-muted me-3">
|
<span class="text-muted me-3">
|
||||||
<i class="bx bx-like"></i> {{ likes || 0 }}
|
<i class="fa-regular fa-eye"></i> {{ views || 0 }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-muted">
|
<span class="text-muted me-3" v-if="likes != null">
|
||||||
<i class="bx bx-comment"></i> {{ comments || 0 }}
|
<i class="bx bx-like"></i> {{ likes }}
|
||||||
|
</span>
|
||||||
|
<span class="text-muted" v-if="comments !== null">
|
||||||
|
<i class="bx bx-comment"></i> {{ comments }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -40,16 +48,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { computed } from 'vue';
|
||||||
props: {
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
// Props 정의
|
||||||
|
const props = defineProps({
|
||||||
img: {
|
img: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: false,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -63,26 +74,38 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
likes: {
|
views: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
likes: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
comments: {
|
comments: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
attachment: {
|
||||||
methods: {
|
type: Boolean,
|
||||||
formatDate(dateString) {
|
default: false,
|
||||||
const date = new Date(dateString);
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// computed 속성
|
||||||
|
const contentColClass = computed(() => {
|
||||||
|
return props.img ? 'col-md-10 col-12' : 'col-md-12';
|
||||||
|
});
|
||||||
|
|
||||||
|
// formattedDate을 computed로 정의
|
||||||
|
const formattedDate = computed(() => {
|
||||||
|
const date = new Date(props.date);
|
||||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(
|
||||||
date.getDate()
|
date.getDate()
|
||||||
).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(
|
).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(
|
||||||
date.getMinutes()
|
date.getMinutes()
|
||||||
).padStart(2, "0")}`;
|
).padStart(2, "0")}`;
|
||||||
},
|
});
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -1,41 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div v-if="posts.length === 0" class="text-center">
|
<div v-if="posts.length === 0" class="text-center">
|
||||||
게시물이 없습니다.
|
<p class="text-muted mt-4">게시물이 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="post in posts" :key="post.id" @click="handleClick(post.id)">
|
<div v-for="post in posts" :key="post.id" @click="handleClick(post.id)">
|
||||||
<BoardCard
|
<BoardCard
|
||||||
:img="post.img"
|
:img="post.img || null"
|
||||||
:category="post.category"
|
:category="post.category || ''"
|
||||||
:title="post.title"
|
:title="post.title"
|
||||||
:content="post.content"
|
:content="post.content"
|
||||||
:date="post.date"
|
:date="post.date"
|
||||||
:likes="post.likes"
|
:views="post.views || 0"
|
||||||
:comments="post.comments"
|
v-bind="getBoardCardProps(post)"
|
||||||
|
:attachment="post.attachment || false"
|
||||||
|
@click="() => goDetail(post.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
import BoardCard from './BoardCard.vue';
|
import BoardCard from './BoardCard.vue';
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
components: {
|
|
||||||
BoardCard,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
posts: {
|
posts: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
default: () => [],
|
||||||
},
|
}
|
||||||
emits: ['click'],
|
});
|
||||||
methods: {
|
|
||||||
handleClick(id) {
|
const emit = defineEmits(['click']);
|
||||||
this.$emit('click', id);
|
|
||||||
},
|
// 상세 페이지 이동
|
||||||
},
|
const goDetail = (id) => {
|
||||||
|
emit('click', id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 없는 데이터는 상위 페이지에서 삭제해도 되게끔 설정
|
||||||
|
const getBoardCardProps = (post) => {
|
||||||
|
const boardCardProps = {};
|
||||||
|
if ('likes' in post) {
|
||||||
|
boardCardProps.likes = post.likes;
|
||||||
|
}
|
||||||
|
if ('comments' in post) {
|
||||||
|
boardCardProps.comments = post.comments;
|
||||||
|
}
|
||||||
|
return boardCardProps;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +1,126 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
<ul class="pagination pagination-rounded justify-content-center">
|
<ul class="pagination pagination-rounded justify-content-center">
|
||||||
<!-- <li class="page-item first">
|
<!-- 첫 페이지 이동 -->
|
||||||
<a class="page-link" href="javascript:void(0);"><i class="tf-icon bx bx-chevrons-left bx-sm"></i></a>
|
<li
|
||||||
</li> -->
|
class="page-item first"
|
||||||
<!-- <li class="page-item prev">
|
@click="emitPageChange(1)"
|
||||||
<a class="page-link" href="javascript:void(0);"><i class="tf-icon bx bx-chevron-left bx-sm"></i></a>
|
:class="{ disabled: isFirstPage }"
|
||||||
</li> -->
|
>
|
||||||
<li class="page-item active">
|
<a class="page-link" href="javascript:void(0);">
|
||||||
<a class="page-link" href="javascript:void(0);">1</a>
|
<i class="tf-icon bx bx-chevrons-left bx-sm"></i>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="javascript:void(0);">2</a>
|
<!-- 이전 페이지 이동 -->
|
||||||
|
<li
|
||||||
|
class="page-item prev"
|
||||||
|
@click="emitPageChange(navigateFirstPage-1)"
|
||||||
|
:class="{ disabled: !hasPreviousPage }"
|
||||||
|
>
|
||||||
|
<a class="page-link" href="javascript:void(0);">
|
||||||
|
<i class="tf-icon bx bx-chevron-left bx-sm"></i>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="javascript:void(0);">3</a>
|
<!-- 페이지 번호들 -->
|
||||||
|
<li
|
||||||
|
v-for="page in navigatepageNums"
|
||||||
|
:key="page"
|
||||||
|
:class="['page-item', { active: page === currentPage }]"
|
||||||
|
@click="emitPageChange(page)"
|
||||||
|
>
|
||||||
|
<a class="page-link" href="javascript:void(0);">{{ page }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="javascript:void(0);">4</a>
|
<!-- 다음 페이지 이동 -->
|
||||||
|
<li
|
||||||
|
class="page-item next"
|
||||||
|
@click="emitPageChange(navigateLastPage+1)"
|
||||||
|
:class="{ disabled: !hasNextPage }"
|
||||||
|
>
|
||||||
|
<a class="page-link" href="javascript:void(0);">
|
||||||
|
<i class="tf-icon bx bx-chevron-right bx-sm"></i>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="javascript:void(0);">5</a>
|
<!-- 마지막 페이지 이동 -->
|
||||||
</li>
|
<li
|
||||||
<li class="page-item next">
|
class="page-item last"
|
||||||
<a class="page-link" href="javascript:void(0);"><i class="tf-icon bx bx-chevron-right bx-sm"></i></a>
|
@click="emitPageChange(pages)"
|
||||||
</li>
|
:class="{ disabled: isLastPage }"
|
||||||
<li class="page-item last">
|
>
|
||||||
<a class="page-link" href="javascript:void(0);"><i class="tf-icon bx bx-chevrons-right bx-sm"></i></a>
|
<a class="page-link" href="javascript:void(0);">
|
||||||
|
<i class="tf-icon bx bx-chevrons-right bx-sm"></i>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
// Props 정의
|
||||||
|
const props = defineProps({
|
||||||
|
currentPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
prePage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nextPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isFirstPage: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isLastPage: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
hasPreviousPage: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
hasNextPage: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
navigatePages: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
navigatepageNums: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
navigateFirstPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
navigateLastPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 이벤트 정의
|
||||||
|
const emit = defineEmits(['update:currentPage']);
|
||||||
|
|
||||||
|
// 페이지 변경 메서드
|
||||||
|
const emitPageChange = (page) => {
|
||||||
|
if (page !== props.currentPage && page >= 1 && page <= props.pages) {
|
||||||
|
emit('update:currentPage', page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
@ -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 bg-white" placeholder="Search" @change="search" />
|
<input type="text" class="form-control" placeholder="Search" @change="search" />
|
||||||
<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>
|
||||||
|
|||||||
0
src/components/user/FindPassword.vue
Normal file
0
src/components/user/FindPassword.vue
Normal file
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<UserFormInput
|
<UserFormInput
|
||||||
title="비밀번호"
|
title="비밀번호"
|
||||||
name="pw"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
:is-alert="passwordAlert"
|
:is-alert="passwordAlert"
|
||||||
@update:data="handlePasswordChange"
|
@update:data="handlePasswordChange"
|
||||||
@ -36,6 +36,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import $api from '@api';
|
||||||
|
import router from '@/router';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import UserFormInput from '@c/input/UserFormInput.vue';
|
import UserFormInput from '@c/input/UserFormInput.vue';
|
||||||
|
|
||||||
@ -44,8 +46,6 @@
|
|||||||
const idAlert = ref(false);
|
const idAlert = ref(false);
|
||||||
const passwordAlert = ref(false);
|
const passwordAlert = ref(false);
|
||||||
|
|
||||||
const emit = defineEmits(['submit']);
|
|
||||||
|
|
||||||
const handleIdChange = value => {
|
const handleIdChange = value => {
|
||||||
id.value = value;
|
id.value = value;
|
||||||
idAlert.value = false;
|
idAlert.value = false;
|
||||||
@ -56,12 +56,23 @@
|
|||||||
passwordAlert.value = false;
|
passwordAlert.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
idAlert.value = id.value.trim() === '';
|
|
||||||
passwordAlert.value = password.value.trim() === '';
|
|
||||||
|
|
||||||
if (!idAlert.value && !passwordAlert.value) {
|
try {
|
||||||
emit('submit', { id: id.value, password: password.value });
|
const response = await $api.post('user/login', {
|
||||||
|
loginId: id.value,
|
||||||
|
password: password.value,
|
||||||
|
remember: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log('로그인 성공', response.data);
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('로그인 실패', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,11 +3,12 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<label
|
<label
|
||||||
for="profilePic"
|
for="profilePic"
|
||||||
class="rounded-circle m-auto border-label-secondary ui-bg-cover position-relative cursor-pointer"
|
class="rounded-circle m-auto ui-bg-cover position-relative cursor-pointer"
|
||||||
id="profileLabel"
|
id="profileLabel"
|
||||||
style="width: 100px; height: 100px; background-image: url(public/img/avatars/default-Profile.jpg)"
|
style="width: 100px; height: 100px; background-image: url(public/img/avatars/default-Profile.jpg); background-repeat: no-repeat;"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<span class="link-danger position-absolute">*</span>
|
<span class="link-danger position-absolute">*</span>
|
||||||
<input type="file" id="profilePic" class="d-none" @change="profileUpload" />
|
<input type="file" id="profilePic" class="d-none" @change="profileUpload" />
|
||||||
<span v-if="profilerr" class="invalid-feedback d-block">{{ profilerr }}</span>
|
<span v-if="profilerr" class="invalid-feedback d-block">{{ profilerr }}</span>
|
||||||
@ -22,8 +23,10 @@
|
|||||||
:useInputGroup="true"
|
:useInputGroup="true"
|
||||||
@update:data="id = $event"
|
@update:data="id = $event"
|
||||||
@update:alert="idAlert = $event"
|
@update:alert="idAlert = $event"
|
||||||
|
@blur="checkIdDuplicate"
|
||||||
:value="id"
|
:value="id"
|
||||||
/>
|
/>
|
||||||
|
<span v-if="idError" class="invalid-feedback d-block">{{ idError }}</span>
|
||||||
|
|
||||||
<UserFormInput
|
<UserFormInput
|
||||||
title="비밀번호"
|
title="비밀번호"
|
||||||
@ -44,16 +47,28 @@
|
|||||||
:is-alert="passwordcheckAlert"
|
:is-alert="passwordcheckAlert"
|
||||||
@update:data="passwordcheck = $event"
|
@update:data="passwordcheck = $event"
|
||||||
@update:alert="passwordcheckAlert = $event"
|
@update:alert="passwordcheckAlert = $event"
|
||||||
|
@blur="checkPw"
|
||||||
:value="passwordcheck"
|
:value="passwordcheck"
|
||||||
/>
|
/>
|
||||||
|
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
|
||||||
|
|
||||||
<UserFormInput
|
<FormSelect
|
||||||
title="비밀번호 힌트"
|
title="비밀번호 힌트"
|
||||||
name="pwhint"
|
name="pwhint"
|
||||||
:is-alert="passwordhintAlert"
|
:is-essential="true"
|
||||||
@update:data="passwordhint = $event"
|
:is-row="false"
|
||||||
@update:alert="passwordhintAlert = $event"
|
:is-label="true"
|
||||||
:value="passwordhint"
|
:data="pwhintList"
|
||||||
|
@update:data="pwhint = $event"
|
||||||
|
/>
|
||||||
|
<UserFormInput
|
||||||
|
title="답변"
|
||||||
|
name="pwhintRes"
|
||||||
|
:is-essential="true"
|
||||||
|
:is-alert="pwhintResAlert"
|
||||||
|
@update:data="pwhintRes = $event"
|
||||||
|
@update:alert="pwhintResAlert = $event"
|
||||||
|
:value="pwhintRes"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
@ -74,6 +89,7 @@
|
|||||||
:is-essential="true"
|
:is-essential="true"
|
||||||
:is-row="false"
|
:is-row="false"
|
||||||
:is-label="true"
|
:is-label="true"
|
||||||
|
:is-common="true"
|
||||||
:data="colorList"
|
:data="colorList"
|
||||||
@update:data="color = $event"
|
@update:data="color = $event"
|
||||||
class="w-50"
|
class="w-50"
|
||||||
@ -84,6 +100,7 @@
|
|||||||
<UserFormInput
|
<UserFormInput
|
||||||
title="생년월일"
|
title="생년월일"
|
||||||
name="birth"
|
name="birth"
|
||||||
|
:type="'date'"
|
||||||
:is-essential="true"
|
:is-essential="true"
|
||||||
:is-alert="birthAlert"
|
:is-alert="birthAlert"
|
||||||
@update:data="birth = $event"
|
@update:data="birth = $event"
|
||||||
@ -98,6 +115,7 @@
|
|||||||
:is-essential="true"
|
:is-essential="true"
|
||||||
:is-row="false"
|
:is-row="false"
|
||||||
:is-label="true"
|
:is-label="true"
|
||||||
|
:is-common="true"
|
||||||
:data="mbtiList"
|
:data="mbtiList"
|
||||||
@update:data="mbti = $event"
|
@update:data="mbti = $event"
|
||||||
class="w-50"
|
class="w-50"
|
||||||
@ -106,12 +124,12 @@
|
|||||||
|
|
||||||
<ArrInput
|
<ArrInput
|
||||||
title="주소"
|
title="주소"
|
||||||
name="arr"
|
name="address"
|
||||||
:isEssential="true"
|
:isEssential="true"
|
||||||
:is-alert="arrAlert"
|
:is-alert="addressAlert"
|
||||||
@update:data="arr = $event"
|
@update:data="handleAddressUpdate"
|
||||||
@update:alert="arrAlert = $event"
|
@update:alert="addressAlert = $event"
|
||||||
:value="arr"
|
:value="address"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UserFormInput
|
<UserFormInput
|
||||||
@ -134,69 +152,81 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
|
import $api from '@api';
|
||||||
import UserFormInput from '@c/input/UserFormInput.vue';
|
import UserFormInput from '@c/input/UserFormInput.vue';
|
||||||
import FormSelect from '@/components/input/FormSelect.vue';
|
import FormSelect from '@c/input/FormSelect.vue';
|
||||||
import ArrInput from '@/components/input/ArrInput.vue';
|
import ArrInput from '@c/input/ArrInput.vue';
|
||||||
|
import { fileMsg } from '@/common/msgEnum';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useToastStore } from '@s/toastStore';
|
||||||
|
|
||||||
|
const pwhintList = ['현재 살고 있는 동네', '가장 기억에 남는 책', '좋아하는 음식'];
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const profile = ref(null);
|
const profile = ref(null);
|
||||||
const profilerr = ref('');
|
const profilerr = ref('');
|
||||||
|
|
||||||
const id = ref('');
|
const id = ref('');
|
||||||
|
const idError = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const passwordcheck = ref('');
|
const passwordcheck = ref('');
|
||||||
const passwordhint = ref('');
|
const passwordcheckError = ref('');
|
||||||
|
const pwhint = ref(0);
|
||||||
|
const pwhintRes = ref('');
|
||||||
const name = ref('');
|
const name = ref('');
|
||||||
const birth = ref('');
|
const birth = ref('');
|
||||||
const arr = ref('');
|
const address = ref('');
|
||||||
|
const detailAddress = ref('');
|
||||||
|
const postcode = ref(''); // 우편번호
|
||||||
const phone = ref('');
|
const phone = ref('');
|
||||||
const color = ref('');
|
const colorList = ref([]);
|
||||||
const mbti = ref('');
|
const mbtiList = ref([]);
|
||||||
|
const color = ref(''); // 선택된 color
|
||||||
|
const mbti = ref(''); // 선택된 MBTI
|
||||||
|
|
||||||
|
const profilAlert = ref(false);
|
||||||
const idAlert = ref(false);
|
const idAlert = ref(false);
|
||||||
|
const idErrorAlert = ref(false);
|
||||||
const passwordAlert = ref(false);
|
const passwordAlert = ref(false);
|
||||||
const passwordcheckAlert = ref(false);
|
const passwordcheckAlert = ref(false);
|
||||||
const passwordhintAlert = ref(false);
|
const passwordcheckErrorAlert = ref(false); // 비밀번호 확인 오류 메시지
|
||||||
|
const pwhintResAlert = ref(false);
|
||||||
const nameAlert = ref(false);
|
const nameAlert = ref(false);
|
||||||
const birthAlert = ref(false);
|
const birthAlert = ref(false);
|
||||||
const arrAlert = ref(false);
|
const addressAlert = ref(false);
|
||||||
const phoneAlert = ref(false);
|
const phoneAlert = ref(false);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const toastStore = useToastStore();
|
||||||
idAlert.value = id.value.trim() === '';
|
|
||||||
passwordAlert.value = password.value.trim() === '';
|
|
||||||
passwordcheckAlert.value = passwordcheck.value.trim() === '';
|
|
||||||
passwordhintAlert.value = passwordhint.value.trim() === '';
|
|
||||||
nameAlert.value = name.value.trim() === '';
|
|
||||||
birthAlert.value = birth.value.trim() === '';
|
|
||||||
arrAlert.value = arr.value.trim() === '';
|
|
||||||
phoneAlert.value = phone.value.trim() === '';
|
|
||||||
|
|
||||||
if (!profile.value) {
|
|
||||||
profilerr.value = '프로필 이미지를 선택해주세요.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 프로필 체크
|
||||||
const profileValid = (size, type) => {
|
const profileValid = (size, type) => {
|
||||||
const maxSize = 5 * 1024 * 1024;
|
const maxSize = 5 * 1024 * 1024;
|
||||||
const validTypes = ['image/jpeg', 'image/png'];
|
const validTypes = ['image/jpeg', 'image/png'];
|
||||||
|
|
||||||
|
// 용량
|
||||||
if (size > maxSize) {
|
if (size > maxSize) {
|
||||||
profilerr.value = '5MB 미만의 파일만 업로드할 수 있습니다.';
|
profilerr.value = fileMsg.FileMaxSizeMsg;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 파일 타입
|
||||||
if (!validTypes.includes(type)) {
|
if (!validTypes.includes(type)) {
|
||||||
profilerr.value = '지원되지 않는 파일 형식입니다. JPEG 또는 PNG만 가능합니다.';
|
profilerr.value = fileMsg.FileNotTypeMsg;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
profilerr.value = '';
|
profilerr.value = '';
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 파일 업로드 시 미리보기
|
||||||
const profileUpload = e => {
|
const profileUpload = e => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
const profileLabel = document.getElementById('profileLabel');
|
const profileLabel = document.getElementById('profileLabel');
|
||||||
|
|
||||||
|
// 사이즈, 파일 타입 안 맞으면 기본 이미지
|
||||||
if (!profileValid(file.size, file.type)) {
|
if (!profileValid(file.size, file.type)) {
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
profileLabel.style.backgroundImage = 'url("public/img/avatars/default-Profile.jpg")';
|
profileLabel.style.backgroundImage = 'url("public/img/avatars/default-Profile.jpg")';
|
||||||
@ -209,8 +239,107 @@
|
|||||||
profile.value = file;
|
profile.value = file;
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorList = ['blue', 'red', 'pink'];
|
// 아이디 중복체크
|
||||||
const mbtiList = ['ISTP', 'ENFP', 'INTJ'];
|
const checkIdDuplicate = async () => {
|
||||||
|
const response = await $api.get(`/user/checkId?memberIds=${id.value}`);
|
||||||
|
|
||||||
|
if (!response.data.data) {
|
||||||
|
idErrorAlert.value = true;
|
||||||
|
idError.value = '이미 사용 중인 아이디입니다.';
|
||||||
|
} else {
|
||||||
|
idErrorAlert.value = false;
|
||||||
|
idError.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Colors = async () => {
|
||||||
|
const response = await $api.get('/user/color');
|
||||||
|
colorList.value = response.data.data.map(item => ({
|
||||||
|
label: item.CMNCODNAM,
|
||||||
|
value: item.CMNCODVAL
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const Mbtis = async () => {
|
||||||
|
const response = await $api.get('/user/mbti');
|
||||||
|
mbtiList.value = response.data.data.map(item => ({
|
||||||
|
label: item.CMNCODNAM,
|
||||||
|
value: item.CMNCODVAL
|
||||||
|
}));
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
Colors();
|
||||||
|
Mbtis();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 주소 업데이트 핸들러
|
||||||
|
const handleAddressUpdate = (addressData) => {
|
||||||
|
address.value = addressData.address;
|
||||||
|
detailAddress.value = addressData.detailAddress;
|
||||||
|
postcode.value = addressData.postcode; // 우편번호
|
||||||
|
};
|
||||||
|
|
||||||
|
// 비밀번호 확인 체크
|
||||||
|
const checkPw = async () => {
|
||||||
|
if (password.value !== passwordcheck.value) {
|
||||||
|
passwordcheckError.value = '비밀번호가 일치하지 않습니다.';
|
||||||
|
passwordcheckErrorAlert.value = true;
|
||||||
|
} else {
|
||||||
|
passwordcheckError.value = '';
|
||||||
|
passwordcheckErrorAlert.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 회원가입
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
|
idAlert.value = id.value.trim() === '';
|
||||||
|
passwordAlert.value = password.value.trim() === '';
|
||||||
|
passwordcheckAlert.value = passwordcheck.value.trim() === '';
|
||||||
|
pwhintResAlert.value = pwhintRes.value.trim() === '';
|
||||||
|
nameAlert.value = name.value.trim() === '';
|
||||||
|
birthAlert.value = birth.value.trim() === '';
|
||||||
|
addressAlert.value = address.value.trim() === '';
|
||||||
|
phoneAlert.value = phone.value.trim() === '';
|
||||||
|
|
||||||
|
// 프로필 이미지 체크
|
||||||
|
if (!profile.value) {
|
||||||
|
profilerr.value = '프로필 이미지를 선택해주세요.';
|
||||||
|
profilAlert.value = true;
|
||||||
|
} else {
|
||||||
|
profilerr.value = '';
|
||||||
|
profilAlert.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profilAlert.value || idAlert.value || idErrorAlert.value || passwordAlert.value || passwordcheckAlert.value ||
|
||||||
|
passwordcheckErrorAlert.value || pwhintResAlert.value || nameAlert.value || birthAlert.value || addressAlert.value || phoneAlert.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('memberIds', id.value);
|
||||||
|
formData.append('memberPwd', password.value);
|
||||||
|
formData.append('memberPwh', pwhintList[pwhint.value]);
|
||||||
|
formData.append('memberPwr', pwhintRes.value);
|
||||||
|
formData.append('memberNam', name.value);
|
||||||
|
formData.append('memberArr', address.value);
|
||||||
|
formData.append('memberDtl', detailAddress.value);
|
||||||
|
formData.append('memberZip', postcode.value);
|
||||||
|
formData.append('memberBth', birth.value);
|
||||||
|
formData.append('memberTel', phone.value);
|
||||||
|
formData.append('memberCol', color.value);
|
||||||
|
formData.append('memberMbt', mbti.value);
|
||||||
|
formData.append('memberPrf', profile.value);
|
||||||
|
|
||||||
|
const response = await $api.post('/user/join', formData, { isFormData : true });
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
toastStore.onToast('등록신청이 완료되었습니다. 관리자 승인 후 이용가능합니다.', 's');
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
70
src/components/user/UserList.vue
Normal file
70
src/components/user/UserList.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 컴포넌트 사용 ex)
|
||||||
|
|
||||||
|
<UserList @user-list-update="handleUserListUpdate" />
|
||||||
|
|
||||||
|
-->
|
||||||
|
<ul class="list-unstyled users-list d-flex align-items-center">
|
||||||
|
<li
|
||||||
|
v-for="(user, index) in userList"
|
||||||
|
:key="index"
|
||||||
|
class="avatar pull-up"
|
||||||
|
:class="{ disabled: user.disabled }"
|
||||||
|
@click="toggleDisable(index)"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-popup="tooltip-custom"
|
||||||
|
data-bs-placement="top"
|
||||||
|
:aria-label="user.MEMBERSEQ"
|
||||||
|
:data-bs-original-title="user.MEMBERSEQ"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="rounded-circle"
|
||||||
|
:src="`http://localhost:10325/upload/img/profile/${user.MEMBERPRF}`"
|
||||||
|
alt="profile"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useUserStore } from '@s/userList';
|
||||||
|
|
||||||
|
const emit = defineEmits();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const userList = ref([]);
|
||||||
|
|
||||||
|
// 사용자 목록 호출
|
||||||
|
onMounted(async () => {
|
||||||
|
await userStore.fetchUserList();
|
||||||
|
userList.value = userStore.userList;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 클릭 시 활성화/비활성화
|
||||||
|
const toggleDisable = (index) => {
|
||||||
|
const user = userList.value[index];
|
||||||
|
if (user) {
|
||||||
|
user.disabled = !user.disabled;
|
||||||
|
emitUserListUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// emit
|
||||||
|
const emitUserListUpdate = () => {
|
||||||
|
const activeUsers = userList.value.filter(user => !user.disabled);
|
||||||
|
const disabledUsers = userList.value.filter(user => user.disabled);
|
||||||
|
|
||||||
|
emit('user-list-update', { activeUsers, disabledUsers });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
/* disabled 클래스를 적용할 때 사용자의 이미지를 흐리게 */
|
||||||
|
.avatar.disabled {
|
||||||
|
opacity: 0.5; /* 흐리게 만들기 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 비활성화된 상태에서 이미지를 회색으로 변환 */
|
||||||
|
.avatar.disabled img {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/components/voteboard/voteCard.vue
Normal file
50
src/components/voteboard/voteCard.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<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" >
|
||||||
|
<div class="w-100">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div class="user-info">
|
||||||
|
<h6 class="mb-1">공공이</h6>
|
||||||
|
</div>
|
||||||
|
<div class="add-btn">
|
||||||
|
<!-- 투표완료시 -->
|
||||||
|
<i class="bx bxs-check-circle link-success"></i>
|
||||||
|
<!-- 투표작성자만 수정/삭제/종료 가능 -->
|
||||||
|
<button type="button" class="bx btn btn-danger">종료</button>
|
||||||
|
<EditBtn />
|
||||||
|
<DeleteBtn />
|
||||||
|
</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>
|
||||||
|
<!-- 투표완료시-->
|
||||||
|
<vote-revote-end />
|
||||||
|
<!-- 투표안했을시-->
|
||||||
|
<vote-card-check />
|
||||||
|
<!-- 투표완/미완 인원 -->
|
||||||
|
<vote-user-list />
|
||||||
|
<!-- 투표 결과 -->
|
||||||
|
<vote-result-list />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import EditBtn from '@c/button/EditBtn.vue';
|
||||||
|
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';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
27
src/components/voteboard/voteCardCheck.vue
Normal file
27
src/components/voteboard/voteCardCheck.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-sm">투표하기</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
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';
|
||||||
|
|
||||||
|
const titleAlert = ref(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style >
|
||||||
|
|
||||||
|
</style>
|
||||||
16
src/components/voteboard/voteCardCheckList.vue
Normal file
16
src/components/voteboard/voteCardCheckList.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="list-group">
|
||||||
|
<label class="list-group-item">
|
||||||
|
<input class="form-check-input me-1" type="checkbox" value="">
|
||||||
|
case1
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
13
src/components/voteboard/voteCardList.vue
Normal file
13
src/components/voteboard/voteCardList.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<card />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import card from '@c/voteboard/voteCard.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
23
src/components/voteboard/voteCompleteUserList.vue
Normal file
23
src/components/voteboard/voteCompleteUserList.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class='bx bxs-user-check link-info'></i>
|
||||||
|
<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 />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import voteCompleteUserListCard from '@c/voteboard/voteCompleteUserListCard.vue';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
13
src/components/voteboard/voteCompleteUserListCard.vue
Normal file
13
src/components/voteboard/voteCompleteUserListCard.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
23
src/components/voteboard/voteInCompleteUserList.vue
Normal file
23
src/components/voteboard/voteInCompleteUserList.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class='bx bxs-user-x link-danger'></i>
|
||||||
|
<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 />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import voteInCompleteUserListCard from '@c/voteboard/voteInCompleteUserListCard.vue';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
13
src/components/voteboard/voteInCompleteUserListCard.vue
Normal file
13
src/components/voteboard/voteInCompleteUserListCard.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
25
src/components/voteboard/voteResultCard.vue
Normal file
25
src/components/voteboard/voteResultCard.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ul class="timeline mb-0">
|
||||||
|
<li class="timeline-item ps-6 ">
|
||||||
|
<span class="timeline-indicator-advanced timeline-indicator-success border-0 shadow-none">
|
||||||
|
<i class="icon-base bx bx-check-circle"></i>
|
||||||
|
</span>
|
||||||
|
<div class="timeline-event ps-1">
|
||||||
|
<div class="timeline-header">
|
||||||
|
<small class="text-success text-uppercase">투표결과</small>
|
||||||
|
</div>
|
||||||
|
<h6 class="my-50">돼지고기 </h6>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
19
src/components/voteboard/voteResultList.vue
Normal file
19
src/components/voteboard/voteResultList.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card mb-6 border border-2 border-primary rounded primary-shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- 1위가 여러개일때 -->
|
||||||
|
<vote-result-random />
|
||||||
|
<!-- 1위가 하나일때-->
|
||||||
|
<vote-result-card />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import voteResultRandom from '@c/voteboard/voteResultRandom.vue';
|
||||||
|
import voteResultCard from '@c/voteboard/voteResultCard.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
16
src/components/voteboard/voteResultRandom.vue
Normal file
16
src/components/voteboard/voteResultRandom.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<h6><i class="icon-base bx bx-error icon-lg link-warning"></i> 1위가 중복 되었습니다</h6>
|
||||||
|
<!-- 중복된 1위 리스트 -->
|
||||||
|
<vote-result-random-list />
|
||||||
|
<div class="d-grid w-100 mt-6">
|
||||||
|
<button class="btn btn-primary" data-bs-target="#upgradePlanModal" data-bs-toggle="modal">랜덤 1위 뽑기</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import voteResultRandomList from "@c/voteboard/voteResultRandomList.vue"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
19
src/components/voteboard/voteResultRandomList.vue
Normal file
19
src/components/voteboard/voteResultRandomList.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ul class="list-unstyled g-2 my-6 ">
|
||||||
|
<li class="mb-1 d-flex align-items-center ">
|
||||||
|
<div class="d-flex align-items-center lh-1 me-4 mb-4 mb-sm-0">
|
||||||
|
<span class="badge badge-dot text-bg-warning me-1"></span> 돼지고기
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
16
src/components/voteboard/voteRevoteEnd.vue
Normal file
16
src/components/voteboard/voteRevoteEnd.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
19
src/components/voteboard/voteUserList.vue
Normal file
19
src/components/voteboard/voteUserList.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<!--투표한 사람 목록 -->
|
||||||
|
<vote-complete-user-list />
|
||||||
|
<!-- 투표안한 사람 목록 -->
|
||||||
|
<vote-in-complete-user-list />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import voteCompleteUserList from '@c/voteboard/voteCompleteUserList.vue';
|
||||||
|
import voteInCompleteUserList from '@c/voteboard/voteInCompleteUserList.vue';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 스타일 정의 */
|
||||||
|
</style>
|
||||||
@ -43,6 +43,18 @@
|
|||||||
<div class="text-truncate">Board</div>
|
<div class="text-truncate">Board</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="menu-item" :class="$route.path.includes('/wordDict') ? 'active' : ''">
|
||||||
|
<RouterLink class="menu-link" to="/wordDict">
|
||||||
|
<i class="menu-icon icon-base bx bx-book-open"></i>
|
||||||
|
<div class="text-truncate">wordDict</div>
|
||||||
|
</RouterLink>
|
||||||
|
</li>
|
||||||
|
<li class="menu-item" :class="$route.path.includes('/voteboard') ? 'active' : ''">
|
||||||
|
<RouterLink class="menu-link" to="/voteboard">
|
||||||
|
<i class="menu-icon icon-base bx bx-box"></i>
|
||||||
|
<div class="text-truncate">voteboard</div>
|
||||||
|
</RouterLink>
|
||||||
|
</li>
|
||||||
<li class="menu-item" :class="$route.path.includes('/vacation') ? 'active' : ''">
|
<li class="menu-item" :class="$route.path.includes('/vacation') ? 'active' : ''">
|
||||||
<RouterLink class="menu-link" to="/vacation">
|
<RouterLink class="menu-link" to="/vacation">
|
||||||
<i class="menu-icon tf-icons bx bx-calendar"></i>
|
<i class="menu-icon tf-icons bx bx-calendar"></i>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import App from './App.vue'
|
|||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import dayjs from '@p/dayjs'
|
import dayjs from '@p/dayjs'
|
||||||
import ToastModal from '@c/modal/ToastModal.vue';
|
import ToastModal from '@c/modal/ToastModal.vue';
|
||||||
|
import common from '@/common/common.js'
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
pinia.use(piniaPersist)
|
pinia.use(piniaPersist)
|
||||||
@ -12,6 +13,7 @@ pinia.use(piniaPersist)
|
|||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
.use(pinia)
|
.use(pinia)
|
||||||
|
.use(common)
|
||||||
.use(dayjs)
|
.use(dayjs)
|
||||||
.component('ToastModal',ToastModal)
|
.component('ToastModal',ToastModal)
|
||||||
.mount('#app')
|
.mount('#app')
|
||||||
|
|||||||
@ -50,6 +50,21 @@ const routes = [
|
|||||||
component: () => import('@v/user/TheRegister.vue'),
|
component: () => import('@v/user/TheRegister.vue'),
|
||||||
meta: { layout: 'NoLayout' },
|
meta: { layout: 'NoLayout' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/voteboard',
|
||||||
|
component: () => import('@v/voteboard/TheVoteBoard.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: () => import('@v/voteboard/voteBoardList.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'write',
|
||||||
|
component: () => import('@v/voteboard/voteboardWrite.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/sample',
|
path: '/sample',
|
||||||
component: () => import('@c/calendar/SampleCalendar.vue'),
|
component: () => import('@c/calendar/SampleCalendar.vue'),
|
||||||
|
|||||||
21
src/stores/userList.js
Normal file
21
src/stores/userList.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
작성자 : 공현지
|
||||||
|
작성일 : 2025-01-24
|
||||||
|
수정자 :
|
||||||
|
수정일 :
|
||||||
|
설명 : 사용자 전체 목록
|
||||||
|
*/
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import axios from "@api";
|
||||||
|
|
||||||
|
export const useUserStore = defineStore("userStore", {
|
||||||
|
state: () => ({
|
||||||
|
userList: [],
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async fetchUserList() {
|
||||||
|
const response = await axios.get('user/allUserList');
|
||||||
|
this.userList = response.data.data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -1,24 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-xxl flex-grow-1 container-p-y">
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
<!-- 검색 -->
|
<div class="row mb-4">
|
||||||
|
<!-- 검색창 -->
|
||||||
|
<div class="col">
|
||||||
<search-bar @update:data="search" />
|
<search-bar @update:data="search" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 리스트 -->
|
<div class="row">
|
||||||
<div class="row g-3">
|
<!-- 새 글쓰기 -->
|
||||||
<div class="mt-8">
|
<div class="mb-4">
|
||||||
<router-link to="/board/write">
|
<router-link to="/board/write">
|
||||||
<WriteButton />
|
<WriteButton />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<board-card :posts="paginatedList" @click="goDetail" />
|
<!-- 공지사항 리스트 -->
|
||||||
|
<div v-if="pagination.currentPage === 1" class="mb-8">
|
||||||
|
<div class="row g-3">
|
||||||
|
<h3>공지사항</h3>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<BoardCardList :posts="noticeList" @click="goDetail" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 일반 리스트 -->
|
||||||
|
<div>
|
||||||
|
<div class="row g-3">
|
||||||
|
<h3 class="col">일반게시판</h3>
|
||||||
|
|
||||||
|
<!-- 셀렉트 박스 -->
|
||||||
|
<div class="col-12 col-md-auto">
|
||||||
|
<select class="form-select" v-model="selectedOrder" @change="handleSortChange">
|
||||||
|
<option value="date">최신날짜</option>
|
||||||
|
<option value="views">조회수</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<BoardCardList :posts="generalList" @click="goDetail" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 페이지네이션 -->
|
<!-- 페이지네이션 -->
|
||||||
|
<div class="row g-3">
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<pagination
|
<Pagination
|
||||||
:current-page="currentPage"
|
:currentPage="pagination.currentPage"
|
||||||
:total-pages="totalPages"
|
:pages="pagination.pages"
|
||||||
@update:page="changePage"
|
:prePage="pagination.prePage"
|
||||||
|
:nextPage="pagination.nextPage"
|
||||||
|
:isFirstPage="pagination.isFirstPage"
|
||||||
|
:isLastPage="pagination.isLastPage"
|
||||||
|
:hasPreviousPage="pagination.hasPreviousPage"
|
||||||
|
:hasNextPage="pagination.hasNextPage"
|
||||||
|
:navigatePages="pagination.navigatePages"
|
||||||
|
:navigatepageNums="pagination.navigatepageNums"
|
||||||
|
:navigateFirstPage="pagination.navigateFirstPage"
|
||||||
|
:navigateLastPage="pagination.navigateLastPage"
|
||||||
|
@update:currentPage="handlePageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -27,7 +69,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import BoardCard from '@/components/list/BoardCardList.vue';
|
import BoardCardList from '@/components/list/BoardCardList.vue';
|
||||||
import Pagination from '@c/pagination/Pagination.vue';
|
import Pagination from '@c/pagination/Pagination.vue';
|
||||||
import SearchBar from '@c/search/SearchBar.vue';
|
import SearchBar from '@c/search/SearchBar.vue';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
@ -35,75 +77,118 @@ import WriteButton from '@c/button/WriteBtn.vue';
|
|||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
|
||||||
// 데이터 초기화
|
// 데이터 초기화
|
||||||
const list = ref([]);
|
const generalList = ref([]);
|
||||||
|
const noticeList = ref([]);
|
||||||
const searchText = ref('');
|
const searchText = ref('');
|
||||||
|
const selectedOrder = ref('date');
|
||||||
|
const sortDirection = ref('desc');
|
||||||
|
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 goDetail = (id) => {
|
const goDetail = (id) => {
|
||||||
console.log('Navigating to ID:', id)
|
|
||||||
router.push({ name: 'BoardDetail', params: { id } });
|
router.push({ name: 'BoardDetail', params: { id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 검색 처리
|
// 검색 처리
|
||||||
const search = (e) => {
|
const search = (e) => {
|
||||||
searchText.value = e.trim();
|
searchText.value = e.trim();
|
||||||
|
fetchGeneralPosts(1);
|
||||||
|
fetchNoticePosts(searchText.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 검색 결과 필터링
|
// 정렬 변경 핸들러
|
||||||
const filteredList = computed(() =>
|
const handleSortChange = () => {
|
||||||
list.value.filter((item) =>
|
fetchGeneralPosts(1);
|
||||||
item.title.toLowerCase().includes(searchText.value.toLowerCase())
|
};
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 페이지네이션 상태
|
// 게시물 데이터 로드(일반)
|
||||||
const currentPage = ref(1); // 현재 페이지 번호
|
const fetchGeneralPosts = async (page = 1) => {
|
||||||
const itemsPerPage = 5; // 한 페이지에 표시할 아이템 수
|
const response = await axios.get("board/general", {
|
||||||
|
params: {
|
||||||
// 현재 페이지 데이터 계산
|
page: page,
|
||||||
const paginatedList = computed(() => {
|
orderBy: selectedOrder.value,
|
||||||
const start = (currentPage.value - 1) * itemsPerPage;
|
sortDirection: sortDirection.value,
|
||||||
const end = start + itemsPerPage;
|
searchKeyword: searchText.value
|
||||||
return filteredList.value.slice(start, end);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 총 페이지 수 계산
|
|
||||||
const totalPages = computed(() => {
|
|
||||||
return Math.ceil(filteredList.value.length / itemsPerPage);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 페이지 변경 함수
|
|
||||||
const changePage = (page) => {
|
|
||||||
if (page >= 1 && page <= totalPages.value) {
|
|
||||||
currentPage.value = page;
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// 게시물 데이터 로드
|
if (response.data && response.data.data) {
|
||||||
const fetchPosts = async () => {
|
const data = response.data.data;
|
||||||
const response = await axios.get("board/general");
|
// 게시물 리스트 업데이트
|
||||||
console.log(response.data.data.list)
|
generalList.value = data.list.map((post, index) => ({
|
||||||
|
|
||||||
if (response.data && response.data.data && Array.isArray(response.data.data.list)) {
|
|
||||||
list.value = response.data.data.list.map((post, index) => ({
|
|
||||||
...post,
|
...post,
|
||||||
id: post.id || index,
|
id: post.id || index,
|
||||||
img: post.img || null,
|
img: post.img || null,
|
||||||
likes: post.likes || 0,
|
views: post.cnt || 0,
|
||||||
comments: post.comments || 0,
|
likes: post.likeCount != null ? post.likeCount : null,
|
||||||
|
comments: post.commentCount != null ? post.commentCount : null,
|
||||||
|
attachment: post.hasAttachment || false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 페이지네이션 정보 업데이트
|
||||||
|
pagination.value = {
|
||||||
|
currentPage: data.pageNum,
|
||||||
|
pages: data.pages,
|
||||||
|
prePage: data.prePage,
|
||||||
|
nextPage: data.nextPage,
|
||||||
|
isFirstPage: data.isFirstPage,
|
||||||
|
isLastPage: data.isLastPage,
|
||||||
|
hasPreviousPage: data.hasPreviousPage,
|
||||||
|
hasNextPage: data.hasNextPage,
|
||||||
|
navigatePages: data.navigatePages,
|
||||||
|
navigatepageNums: data.navigatepageNums,
|
||||||
|
navigateFirstPage: data.navigateFirstPage,
|
||||||
|
navigateLastPage: data.navigateLastPage
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.error("데이터 오류:", response.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 게시물 데이터 로드(공지사항)
|
||||||
|
const fetchNoticePosts = async () => {
|
||||||
|
const response = await axios.get("board/notices", {
|
||||||
|
params: {
|
||||||
|
searchKeyword: searchText.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data && response.data.data && Array.isArray(response.data.data)) {
|
||||||
|
noticeList.value = response.data.data.map((post, index) => ({
|
||||||
|
...post,
|
||||||
|
id: post.id || index,
|
||||||
|
img: post.img || null,
|
||||||
|
views: post.cnt || 0,
|
||||||
|
attachment: post.hasAttachment || false,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
console.error("Unexpected API response structure:", response.data);
|
console.error("데이터 오류:", response.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 페이지 변경
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
if (page !== pagination.value.currentPage) {
|
||||||
|
fetchGeneralPosts(page);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 데이터 로드
|
// 데이터 로드
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchPosts();
|
fetchGeneralPosts();
|
||||||
|
fetchNoticePosts();
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 필요에 따라 스타일 추가 */
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -5,17 +5,19 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<!-- 프로필 헤더 -->
|
<!-- 프로필 헤더 -->
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<BoardProfile :boardId="currentBoardId.value" :profileName="profileName" />
|
<BoardProfile :boardId="currentBoardId" :profileName="profileName" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 게시글 내용 -->
|
<!-- 게시글 내용 -->
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="mb-4">{{ boardTitle }}</h5>
|
<h5 class="mb-4">{{ boardTitle }}</h5>
|
||||||
<!-- HTML 콘텐츠 렌더링 -->
|
<!-- HTML 콘텐츠 렌더링 -->
|
||||||
<div class="board-content" v-html="boardContent"></div>
|
<div class="board-content text-body" style="line-height: 1.6;" v-html="$common.contentToHtml(boardContent)">
|
||||||
|
|
||||||
|
</div>
|
||||||
<!-- 첨부파일 목록 -->
|
<!-- 첨부파일 목록 -->
|
||||||
<ul v-if="attachments.length" class="attachments mt-4">
|
<ul v-if="attachments.length" class="attachments mt-4 list-unstyled">
|
||||||
<li v-for="(attachment, index) in attachments" :key="index">
|
<li v-for="(attachment, index) in attachments" :key="index" class="mb-2">
|
||||||
<a :href="attachment.url" target="_blank">{{ attachment.name }}</a>
|
<a :href="attachment.url" target="_blank" class="text-decoration-none">{{ attachment.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<!-- 댓글 영역 -->
|
<!-- 댓글 영역 -->
|
||||||
@ -23,10 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 수정 버튼 -->
|
<!-- 수정 버튼 -->
|
||||||
<div class="card-footer d-flex justify-content-end">
|
<div class="card-footer d-flex justify-content-end">
|
||||||
<button
|
<button class="btn btn-primary" @click="goToEditPage">
|
||||||
class="btn btn-primary"
|
|
||||||
@click="goToEditPage"
|
|
||||||
>
|
|
||||||
글 수정
|
글 수정
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -42,11 +41,13 @@ import BoardProfile from '@c/board/BoardProfile.vue';
|
|||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
import Quill from 'quill';
|
||||||
|
|
||||||
// 게시물 데이터 상태
|
// 게시물 데이터 상태
|
||||||
const profileName = ref('익명 사용자');
|
const profileName = ref('익명 사용자');
|
||||||
const boardTitle = ref('제목 없음');
|
const boardTitle = ref('제목 없음');
|
||||||
const boardContent = ref('내용 없음');
|
const boardContent = ref('');
|
||||||
|
const convertedContent = ref('내용 없음');
|
||||||
const comments = ref([]);
|
const comments = ref([]);
|
||||||
const attachments = ref([]);
|
const attachments = ref([]);
|
||||||
|
|
||||||
@ -54,7 +55,6 @@ const attachments = ref([]);
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const currentBoardId = ref(Number(route.params.id));
|
const currentBoardId = ref(Number(route.params.id));
|
||||||
console.log(currentBoardId.value)
|
|
||||||
|
|
||||||
// 글 수정 페이지로 이동
|
// 글 수정 페이지로 이동
|
||||||
const goToEditPage = () => {
|
const goToEditPage = () => {
|
||||||
@ -71,7 +71,8 @@ const fetchBoardDetails = async () => {
|
|||||||
const boardDetail = data.boardDetail || {};
|
const boardDetail = data.boardDetail || {};
|
||||||
profileName.value = boardDetail.author || '익명 사용자';
|
profileName.value = boardDetail.author || '익명 사용자';
|
||||||
boardTitle.value = boardDetail.title || '제목 없음';
|
boardTitle.value = boardDetail.title || '제목 없음';
|
||||||
boardContent.value = boardDetail.content || '내용 없음';
|
boardContent.value = boardDetail.content || '';
|
||||||
|
|
||||||
|
|
||||||
attachments.value = data.attachments || [];
|
attachments.value = data.attachments || [];
|
||||||
comments.value = data.comments || [];
|
comments.value = data.comments || [];
|
||||||
@ -83,7 +84,6 @@ const fetchBoardDetails = async () => {
|
|||||||
|
|
||||||
// 컴포넌트 마운트 시 데이터 로드
|
// 컴포넌트 마운트 시 데이터 로드
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('Route Params:', route.params);
|
|
||||||
fetchBoardDetails();
|
fetchBoardDetails();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -9,37 +9,80 @@
|
|||||||
|
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<FormInput title="제목" name="title" :is-essential="true" :is-alert="titleAlert" @update:data="title = $event" />
|
|
||||||
|
|
||||||
<FormSelect title="카테고리" name="cate" :is-essential="true" :data="categoryList" @update:data="category = $event" />
|
|
||||||
|
|
||||||
<FormInput
|
<FormInput
|
||||||
v-show="category == 1"
|
title="제목"
|
||||||
|
name="title"
|
||||||
|
:is-essential="true"
|
||||||
|
:is-alert="titleAlert"
|
||||||
|
v-model="title"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 카테고리 선택 -->
|
||||||
|
<div class="mb-4 d-flex align-items-center">
|
||||||
|
<label class="col-md-2 col-form-label">카테고리 <span class="text-danger">*</span></label>
|
||||||
|
<div class="d-flex flex-wrap align-items-center mt-3 ms-1">
|
||||||
|
<div
|
||||||
|
v-for="(categoryName, index) in categoryList"
|
||||||
|
:key="index"
|
||||||
|
class="form-check me-3"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
:id="`category-${index}`"
|
||||||
|
:value="index"
|
||||||
|
v-model="category"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" :for="`category-${index}`">
|
||||||
|
{{ categoryName }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 비밀번호 필드 -->
|
||||||
|
<div v-if="category === 1" class="mb-4">
|
||||||
|
<FormInput
|
||||||
title="비밀번호"
|
title="비밀번호"
|
||||||
name="pw"
|
name="pw"
|
||||||
type="password"
|
type="password"
|
||||||
:is-essential="true"
|
:is-essential="true"
|
||||||
:is-alert="passwordAlert"
|
:is-alert="passwordAlert"
|
||||||
@update:data="password = $event"
|
v-model="password"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormFile title="첨부파일" name="files" :is-alert="attachFilesAlert" @update:data="attachFiles = $event" />
|
<FormFile
|
||||||
|
title="첨부파일"
|
||||||
|
name="files"
|
||||||
|
:is-alert="attachFilesAlert"
|
||||||
|
@update:data="attachFiles = $event"
|
||||||
|
@update:isValid="isFileValid = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="html5-tel-input" class="col-md-2 col-form-label">
|
<label for="html5-tel-input" class="col-md-2 col-form-label">
|
||||||
내용
|
내용
|
||||||
<span class="text-red">*</span>
|
<span class="text-danger">*</span>
|
||||||
<div class="invalid-feedback" :class="contentAlert ? 'display-block' : ''">내용을 확인해주세요.</div>
|
<div class="invalid-feedback" :class="contentAlert ? 'display-block' : ''">내용을 확인해주세요.</div>
|
||||||
</label>
|
</label>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<!-- <TEditor @update:data="content = $event"/> -->
|
|
||||||
<QEditor @update:data="content = $event" />
|
<QEditor @update:data="content = $event" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4 d-flex justify-content-end">
|
<div class="mb-4 d-flex justify-content-end">
|
||||||
<button type="button" class="btn btn-info right" @click="goList"><i class='bx bx-left-arrow-alt'></i></button>
|
<button type="button" class="btn btn-info" @click="goList">
|
||||||
<button type="button" class="btn btn-primary ms-1" @click="write"><i class='bx bx-check'></i></button>
|
<i class="bx bx-left-arrow-alt"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary ms-1"
|
||||||
|
@click="write"
|
||||||
|
:disabled="!isFileValid"
|
||||||
|
>
|
||||||
|
<i class="bx bx-check"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -49,114 +92,77 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import QEditor from '@c/editor/QEditor.vue';
|
import QEditor from '@c/editor/QEditor.vue';
|
||||||
import TEditor from '@c/editor/TEditor.vue';
|
|
||||||
import FormInput from '@c/input/FormInput.vue';
|
import FormInput from '@c/input/FormInput.vue';
|
||||||
import FormSelect from '@c/input/FormSelect.vue';
|
|
||||||
import FormFile from '@c/input/FormFile.vue';
|
import FormFile from '@c/input/FormFile.vue';
|
||||||
import { ref, watch } from 'vue';
|
import { getCurrentInstance, ref } from 'vue';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import axios from '@api';
|
import axios from '@api';
|
||||||
|
|
||||||
const categoryList = ['자유', '익명', '공지사항'];
|
const categoryList = ['자유', '익명', '공지사항']; // 카테고리 이름
|
||||||
// input 경고문 만들어야함!!
|
|
||||||
const title = ref('');
|
const title = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const category = ref(0);
|
const category = ref(0); // 기본값 0
|
||||||
const content = ref('');
|
const content = ref('');
|
||||||
const attachFiles = ref(null);
|
const attachFiles = ref(null);
|
||||||
|
const isFileValid = ref(true); // 파일 유효성 상태 추가
|
||||||
|
|
||||||
//input 경고창 관리
|
const titleAlert = ref(false);
|
||||||
const titleAlert = ref(true);
|
|
||||||
const passwordAlert = ref(false);
|
const passwordAlert = ref(false);
|
||||||
const contentAlert = ref(false);
|
const contentAlert = ref(false);
|
||||||
const attachFilesAlert = ref(false);
|
const attachFilesAlert = ref(false);
|
||||||
|
|
||||||
|
const { appContext } = getCurrentInstance();
|
||||||
|
const $common = appContext.config.globalProperties.$common; // $common에 접근
|
||||||
|
|
||||||
const goList = () => {
|
const goList = () => {
|
||||||
// 목록으로 이동 나중엔 페이지 정보 ,검색 정보도 붙여야됨
|
|
||||||
router.push('/board');
|
router.push('/board');
|
||||||
};
|
};
|
||||||
|
|
||||||
const write = async () => {
|
const write = async () => {
|
||||||
// 입력값 유효성 검사
|
titleAlert.value = !title.value;
|
||||||
if (!title.value) {
|
passwordAlert.value = category.value === 1 && !password.value;
|
||||||
titleAlert.value = true;
|
contentAlert.value = !content.value;
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
titleAlert.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category.value === 1 && !password.value) {
|
if (titleAlert.value || passwordAlert.value || contentAlert.value || !isFileValid.value) {
|
||||||
passwordAlert.value = true;
|
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
passwordAlert.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!content.value) {
|
|
||||||
contentAlert.value = true;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
contentAlert.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 게시물 작성 데이터 준비
|
|
||||||
const boardData = {
|
const boardData = {
|
||||||
LOCBRDTTL: title.value,
|
LOCBRDTTL: title.value,
|
||||||
LOCBRDCON: content.value,
|
LOCBRDCON: $common.deltaAsJson(content.value),
|
||||||
LOCBRDPWD: category.value === 1 ? password.value : null,
|
LOCBRDPWD: category.value === 1 ? password.value : null,
|
||||||
LOCBRDTYP: category.value === 1 ? 'S' : 'F', //공지사항 추가해야함!!
|
LOCBRDTYP: category.value === 1 ? 'S' : 'F',
|
||||||
// MEMBERSEQ: 로그인이용자 id(세션)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 게시물 작성 API 호출
|
|
||||||
const { data: boardResponse } = await axios.post('board', boardData);
|
const { data: boardResponse } = await axios.post('board', boardData);
|
||||||
const boardId = boardResponse.data.boardId;
|
const boardId = boardResponse.data;
|
||||||
|
|
||||||
// 첨부파일 처리
|
|
||||||
if (attachFiles.value && attachFiles.value.length > 0) {
|
if (attachFiles.value && attachFiles.value.length > 0) {
|
||||||
for (const file of attachFiles.value) {
|
for (const file of attachFiles.value) {
|
||||||
const realName = file.name.substring(0, file.name.lastIndexOf('.'));
|
|
||||||
const fileInfo = {
|
|
||||||
path: "/uploads", // 파일 경로 (수정 필요)
|
|
||||||
originalName: realName, // 확장자를 제외한 파일명
|
|
||||||
extension: file.name.split('.').pop(), // 파일 확장자
|
|
||||||
registrantId: 1, // 등록자 ID (수정 필요)
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file); // 첨부 파일
|
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
|
||||||
formData.append("CMNFLEPAT", fileInfo.path); // 파일 경로
|
|
||||||
formData.append("CMNFLENAM", fileInfo.originalName); // 파일 명(확장자제외)
|
|
||||||
formData.append("CMNFLEORG", fileInfo.originalName); // 원본 파일명(확장자제외)
|
|
||||||
formData.append("CMNFLEEXT", fileInfo.extension); // 파일 확장자
|
|
||||||
formData.append("CMNFLESIZ", file.size); // 파일 크기
|
|
||||||
formData.append("CMNFLEREG", fileInfo.registrantId); // 등록자 ID
|
|
||||||
|
|
||||||
const response = await axios.post(`board/${boardId}/attachments`, formData, {
|
formData.append('CMNBRDSEQ', boardId);
|
||||||
|
formData.append('CMNFLEORG', fileNameWithoutExt);
|
||||||
|
formData.append('CMNFLEEXT', file.name.split('.').pop());
|
||||||
|
formData.append('CMNFLESIZ', file.size);
|
||||||
|
formData.append('CMNFLEPAT', 'boardfile');
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
await axios.post(`board/${boardId}/attachments`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
alert("게시물이 작성되었습니다.");
|
alert('게시물이 작성되었습니다.');
|
||||||
goList();
|
goList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert("게시물 작성 중 오류가 발생했습니다.");
|
alert('게시물 작성 중 오류가 발생했습니다.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.text-red {
|
|
||||||
color: red;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<div class="d-flex justify-content-center align-items-center vh-100">
|
<div class="d-flex justify-content-center align-items-center vh-100">
|
||||||
<div class="container container-p-y rounded bg-white" style="max-width: 500px">
|
<div class="container container-p-y rounded bg-white" style="max-width: 500px">
|
||||||
<LogoHeader title="LOCALNET" />
|
<LogoHeader title="LOCALNET" />
|
||||||
<LoginForm @submit="handleSubmit" />
|
<LoginForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -11,9 +11,6 @@
|
|||||||
import LoginForm from '@c/user/LoginForm.vue';
|
import LoginForm from '@c/user/LoginForm.vue';
|
||||||
import LogoHeader from '@c/user/LogoHeader.vue';
|
import LogoHeader from '@c/user/LogoHeader.vue';
|
||||||
|
|
||||||
const handleSubmit = async ({ id, password }) => {
|
|
||||||
console.log('Login');
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="d-flex justify-content-center align-items-center">
|
<div class="d-flex justify-content-center align-items-center">
|
||||||
<div class="container rounded bg-white my-10 py-10" style="max-width: 500px">
|
<div class="container rounded bg-white my-10 py-10" style="max-width: 500px">
|
||||||
<RegisterForm @submit="handleSubmit" />
|
<RegisterForm/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -9,9 +9,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import RegisterForm from '@c/user/RegisterForm.vue';
|
import RegisterForm from '@c/user/RegisterForm.vue';
|
||||||
|
|
||||||
const handleSubmit = async (formData) => {
|
|
||||||
console.log('Register');
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
10
src/views/voteboard/TheVoteBoard.vue
Normal file
10
src/views/voteboard/TheVoteBoard.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name : 'voteboard',
|
||||||
|
inheritAttrs : false,
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
63
src/views/voteboard/voteBoardList.vue
Normal file
63
src/views/voteboard/voteBoardList.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
|
<div class="row g-3">
|
||||||
|
<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">
|
||||||
|
<input class="form-check-input" type="checkbox" value="" id="deliveryAdd" checked="">
|
||||||
|
<label class="form-check-label" for="deliveryAdd">내가 한 투표</label>
|
||||||
|
</div>
|
||||||
|
<!-- 투표마감/투표중 셀렉트 -->
|
||||||
|
<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 />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- <div class="mt-8">
|
||||||
|
<pagination />
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { getCurrentInstance, onMounted, ref } from 'vue';
|
||||||
|
// import Pagination from '@/components/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 titleAlert = ref(false);
|
||||||
|
const addContent = ref('');
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
getBoardList();
|
||||||
|
})
|
||||||
|
const getBoardList = () =>{
|
||||||
|
$api.get('worddict/getWordList').then((res)=>{
|
||||||
|
boardList.value = res.data.data.data;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
39
src/views/voteboard/voteboardWrite.vue
Normal file
39
src/views/voteboard/voteboardWrite.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<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>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import UserList from "@c/user/UserList.vue";
|
||||||
|
|
||||||
|
const activeUsers = ref([]); // 활성화된 사용자 목록
|
||||||
|
const disabledUsers = ref([]); // 비활성화된 사용자 목록
|
||||||
|
|
||||||
|
// UserList에서 받은 데이터를 처리
|
||||||
|
const handleUserListUpdate = ({ activeUsers, disabledUsers }) => {
|
||||||
|
activeUsers.value = activeUsers;
|
||||||
|
disabledUsers.value = disabledUsers;
|
||||||
|
console.log('활성화목록>>',activeUsers)
|
||||||
|
console.log('비활성목록>>',disabledUsers)
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue
Block a user