203 lines
7.4 KiB
Vue
203 lines
7.4 KiB
Vue
<template>
|
|
<div class="container-xxl flex-grow-1 container-p-y">
|
|
<div class="card">
|
|
<div class="pb-4 rounded-top">
|
|
<div class="container py-12 px-xl-10 px-4" style="padding-bottom: 0px !important">
|
|
<h3 class="text-center mb-2 mt-4">글 작성</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-12">
|
|
<div class="card-body">
|
|
<!-- 제목 입력 -->
|
|
<FormInput
|
|
title="제목"
|
|
name="title"
|
|
:is-essential="true"
|
|
:is-alert="titleAlert"
|
|
v-model="title"
|
|
@update:alert="titleAlert = $event"
|
|
@input="validateTitle"
|
|
/>
|
|
|
|
<!-- 카테고리 선택 -->
|
|
<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="(category, index) in categoryList"
|
|
:key="index"
|
|
class="form-check me-3"
|
|
>
|
|
<input
|
|
class="form-check-input"
|
|
type="radio"
|
|
:id="`category-${index}`"
|
|
:value="category.CMNCODVAL"
|
|
v-model="categoryValue"
|
|
@change="categoryAlert = false"
|
|
/>
|
|
<label class="form-check-label" :for="`category-${index}`">
|
|
{{ category.CMNCODNAM }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">
|
|
카테고리를 선택해주세요.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 비밀번호 필드 (익명게시판 선택 시 활성화) -->
|
|
<div v-if="categoryValue === 300102" class="mb-4">
|
|
<FormInput
|
|
title="비밀번호"
|
|
name="pw"
|
|
type="password"
|
|
:is-essential="true"
|
|
:is-alert="passwordAlert"
|
|
v-model="password"
|
|
@update:alert="passwordAlert = $event"
|
|
@input="validatePassword"
|
|
/>
|
|
</div>
|
|
|
|
<FormFile
|
|
title="첨부파일"
|
|
name="files"
|
|
:is-alert="attachFilesAlert"
|
|
@update:data="attachFiles = $event"
|
|
@update:isValid="isFileValid = $event"
|
|
/>
|
|
|
|
<!-- 내용 입력 (에디터) -->
|
|
<div class="mb-4">
|
|
<label class="col-md-2 col-form-label">
|
|
내용 <span class="text-danger">*</span>
|
|
</label>
|
|
<div class="col-md-12">
|
|
<QEditor @update:data="updateContent" />
|
|
</div>
|
|
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">
|
|
내용을 입력해주세요.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4 d-flex justify-content-end">
|
|
<BackButton @click="goList" />
|
|
<SaveButton @click="write" :isEnabled="isFileValid" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import QEditor from '@c/editor/QEditor.vue';
|
|
import FormInput from '@c/input/FormInput.vue';
|
|
import FormFile from '@c/input/FormFile.vue';
|
|
import { getCurrentInstance, ref, onMounted } from 'vue';
|
|
import router from '@/router';
|
|
import axios from '@api';
|
|
import SaveButton from '@c/button/SaveBtn.vue';
|
|
import BackButton from '@c/button/BackBtn.vue'
|
|
import { useToastStore } from '@s/toastStore';
|
|
|
|
const toastStore = useToastStore();
|
|
const categoryList = ref([]);
|
|
const title = ref('');
|
|
const password = ref('');
|
|
const categoryValue = ref(null);
|
|
const content = ref('');
|
|
const attachFiles = ref(null);
|
|
const isFileValid = ref(true);
|
|
|
|
const titleAlert = ref(false);
|
|
const passwordAlert = ref(false);
|
|
const contentAlert = ref(false);
|
|
const categoryAlert = ref(false);
|
|
const attachFilesAlert = ref(false);
|
|
|
|
const { appContext } = getCurrentInstance();
|
|
const $common = appContext.config.globalProperties.$common;
|
|
|
|
const fetchCategories = async () => {
|
|
try {
|
|
const response = await axios.get('board/categories');
|
|
categoryList.value = response.data.data;
|
|
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
|
|
if (freeCategory) {
|
|
categoryValue.value = freeCategory.CMNCODVAL;
|
|
}
|
|
} catch (error) {
|
|
console.error('카테고리 불러오기 오류:', error);
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
fetchCategories();
|
|
});
|
|
|
|
// 제목 유효성 검사: 공백만 입력하면 경고 유지, 문자 포함 시 정상
|
|
const validateTitle = () => {
|
|
titleAlert.value = title.value.trim().length === 0;
|
|
};
|
|
|
|
// 비밀번호 유효성 검사: 공백 입력 방지
|
|
const validatePassword = () => {
|
|
password.value = password.value.replace(/\s/g, ""); // 공백 제거
|
|
passwordAlert.value = password.value.length === 0;
|
|
};
|
|
|
|
// 에디터에서 업데이트된 데이터를 반영하는 함수
|
|
const updateContent = (data) => {
|
|
let rawText = '';
|
|
|
|
if (typeof data === 'object' && data.ops) {
|
|
rawText = data.ops.map(op => (typeof op.insert === 'string' ? op.insert : '')).join('').trim();
|
|
} else if (typeof data === 'string') {
|
|
rawText = data.replace(/(<([^>]+)>)/gi, "").trim();
|
|
} else {
|
|
rawText = '';
|
|
}
|
|
|
|
content.value = rawText.length > 0 ? data : "";
|
|
contentAlert.value = rawText.length === 0;
|
|
};
|
|
|
|
/** 페이지 이동 (목록으로 이동) */
|
|
const goList = () => {
|
|
router.push('/board');
|
|
};
|
|
|
|
const write = async () => {
|
|
validateTitle();
|
|
validatePassword();
|
|
|
|
categoryAlert.value = !categoryValue.value;
|
|
contentAlert.value = content.value.length === 0;
|
|
|
|
if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const boardData = {
|
|
LOCBRDTTL: title.value,
|
|
LOCBRDCON: $common.deltaAsJson(content.value),
|
|
LOCBRDPWD: categoryValue.value === 300102 ? password.value : null,
|
|
LOCBRDTYP: categoryValue.value
|
|
};
|
|
|
|
const { data: boardResponse } = await axios.post('board', boardData);
|
|
const boardId = boardResponse.data;
|
|
|
|
toastStore.onToast('게시물이 작성되었습니다.', 's');
|
|
goList();
|
|
} catch (error) {
|
|
console.error(error);
|
|
toastStore.onToast('게시물 작성 중 오류가 발생했습니다.', 'e');
|
|
}
|
|
};
|
|
</script>
|