Merge remote-tracking branch 'origin/main' into board-comment-2
BIN
public/img/mbti/enfj.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/img/mbti/enfp.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/img/mbti/entj.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
public/img/mbti/entp.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
public/img/mbti/esfj.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/img/mbti/esfp.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/img/mbti/est.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
public/img/mbti/estp.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/img/mbti/infj.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/img/mbti/infp.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/img/mbti/intj.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
public/img/mbti/intp.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/img/mbti/isfj.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
public/img/mbti/isfp.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/img/mbti/istj.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/img/mbti/istp.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
@ -15,6 +15,7 @@
|
||||
:disabled="disabled"
|
||||
:min="min"
|
||||
@focusout="$emit('focusout', modelValue)"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">
|
||||
{{ title }}을 확인해주세요.
|
||||
@ -92,11 +93,6 @@ const inputValue = ref(props.modelValue);
|
||||
// 부모로 데이터 업데이트
|
||||
watch(inputValue, (newValue) => {
|
||||
emits('update:modelValue', newValue);
|
||||
|
||||
// 값이 입력될 때 `alert`를 false로 설정
|
||||
if (newValue.trim() !== '') {
|
||||
emits('update:alert', false);
|
||||
}
|
||||
});
|
||||
|
||||
// 초기값 동기화
|
||||
@ -106,6 +102,13 @@ watch(() => props.modelValue, (newValue) => {
|
||||
}
|
||||
});
|
||||
|
||||
const handleInput = (event) => {
|
||||
const newValue = event.target.value.slice(0, props.maxlength);
|
||||
|
||||
if (newValue.trim() !== '') {
|
||||
emits('update:alert', false);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@ -4,19 +4,30 @@
|
||||
{{ title }}
|
||||
<span :class="isEssential ? 'link-danger' : 'none'">*</span>
|
||||
</label>
|
||||
<div :class="isRow ? 'col-md-10' : 'col-md-12'">
|
||||
<select class="form-select" :id="name" v-model="selectData" :disabled="disabled">
|
||||
<option v-for="(item, i) in data" :key="i" :value="isCommon ? item.value : i">
|
||||
<div :class="isRow ? 'col-md-10' : 'col-md-12'" class="d-flex gap-2 align-items-center">
|
||||
<select class="form-select" :id="name" v-model="selectData" :disabled="disabled" :style="isColor ? { color: selected } : {}">
|
||||
<option v-for="(item, i) in data" :key="i" :value="isCommon ? item.value : i" :style="isColor ? { color: item.label } : {}">
|
||||
{{ isCommon ? item.label : item }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div v-if="isColor && selected"
|
||||
class="w-px-40 h-px-30"
|
||||
:style="{backgroundColor: selected}">
|
||||
</div>
|
||||
|
||||
<img v-if="isMbti && selected"
|
||||
role="img"
|
||||
class="w-px-30 h-px-40"
|
||||
:src="`/img/mbti/${selected.toLowerCase()}.png`"
|
||||
alt="MBTI image"/>
|
||||
</div>
|
||||
<div v-if="isAlert" class="invalid-feedback">{{ title }}을 확인해주세요.</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
@ -67,7 +78,17 @@ const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
isColor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
isMbti: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:data']);
|
||||
@ -93,4 +114,13 @@ watch(() => props.data, (newData) => {
|
||||
watch(selectData, (newValue) => {
|
||||
emit('update:data', newValue);
|
||||
});
|
||||
|
||||
|
||||
const selected = computed(() => {
|
||||
const selectedItem = props.data.find(item =>
|
||||
props.isCommon ? item.value === selectData.value : props.data.indexOf(item) === selectData.value
|
||||
);
|
||||
return selectedItem ? selectedItem.label : null;
|
||||
});
|
||||
|
||||
</script>
|
||||
@ -68,20 +68,21 @@
|
||||
:is-alert="passwordcheckAlert"
|
||||
@update:data="passwordcheck = $event"
|
||||
@update:alert="passwordcheckAlert = $event"
|
||||
@blur="checkPw"
|
||||
@input="checkPw"
|
||||
:value="passwordcheck"
|
||||
/>
|
||||
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
|
||||
|
||||
<div class="d-grid gap-2 mt-5 mb-5">
|
||||
<SaveBtn @click="handleNewPassword" />
|
||||
<p v-if="pwErrMsg" class="invalid-feedback d-block mb-0">{{ pwErrMsg }}</p>
|
||||
<div class="d-flex gap-2 mt-7 mb-3">
|
||||
<BackBtn class=" w-50" @click="handleback"/>
|
||||
<SaveBtn class="w-50" @click="handleNewPassword" />
|
||||
</div>
|
||||
<p v-if="pwErrMsg" class="invalid-feedback d-block mb-0">{{ pwErrMsg }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import $api from '@api';
|
||||
import commonApi from '@/common/commonApi';
|
||||
import { useRouter } from 'vue-router';
|
||||
@ -112,7 +113,7 @@
|
||||
|
||||
const passwordAlert = ref(false);
|
||||
const passwordcheckAlert = ref(false);
|
||||
const passwordcheckErrorAlert = ref(false);
|
||||
const passwordMismatch = ref(false);
|
||||
|
||||
const { pwhintList } = commonApi({
|
||||
loadPwhint: true,
|
||||
@ -127,6 +128,13 @@
|
||||
router.push('/login');
|
||||
}
|
||||
|
||||
// 비밀번호와 비밀번호 확인이 변경될 때마다 유효성 검사
|
||||
watch([password, passwordcheck], () => {
|
||||
if (passwordcheck.value !== '') {
|
||||
checkPw();
|
||||
}
|
||||
});
|
||||
|
||||
// 아이디, 생년월일, 비밀번호 힌트, 답변이 일치하는 member 재설정 input 보이기
|
||||
const handleSubmit = async () => {
|
||||
userCheckMsg.value = '';
|
||||
@ -153,13 +161,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
const checkPw = async () => {
|
||||
const checkPw = () => {
|
||||
if (password.value !== passwordcheck.value) {
|
||||
passwordcheckError.value = '비밀번호가 일치하지 않습니다.';
|
||||
passwordcheckErrorAlert.value = true;
|
||||
passwordMismatch.value = true;
|
||||
} else {
|
||||
passwordcheckError.value = '';
|
||||
passwordcheckErrorAlert.value = false;
|
||||
passwordMismatch.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -168,8 +176,10 @@
|
||||
pwErrMsg.value = '';
|
||||
passwordAlert.value = password.value.trim() === '';
|
||||
passwordcheckAlert.value = passwordcheck.value.trim() === '';
|
||||
|
||||
checkPw();
|
||||
if (passwordAlert.value || passwordcheckAlert.value || passwordcheckErrorAlert.value) {
|
||||
|
||||
if (passwordAlert.value || passwordcheckAlert.value || passwordMismatch.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -192,6 +202,5 @@
|
||||
toastStore.onToast('비밀번호가 재설정 되었습니다.', 's');
|
||||
router.push('/login');
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -92,6 +92,7 @@
|
||||
:is-row="false"
|
||||
:is-label="true"
|
||||
:is-common="true"
|
||||
:is-color="true"
|
||||
:data="colorList"
|
||||
@update:data="color = $event"
|
||||
class="w-50"
|
||||
@ -118,6 +119,7 @@
|
||||
:is-row="false"
|
||||
:is-label="true"
|
||||
:is-common="true"
|
||||
:is-mbti="true"
|
||||
:data="mbtiList"
|
||||
@update:data="mbti = $event"
|
||||
class="w-50"
|
||||
|
||||
@ -9,12 +9,15 @@
|
||||
|
||||
<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"
|
||||
/>
|
||||
|
||||
<!-- 카테고리 선택 -->
|
||||
@ -32,16 +35,19 @@
|
||||
: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 ? 'display-block' : ''">카테고리를 선택해주세요.</div>
|
||||
<div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">
|
||||
카테고리를 선택해주세요.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 비밀번호 필드 -->
|
||||
<!-- 비밀번호 필드 (익명게시판 선택 시 활성화) -->
|
||||
<div v-if="categoryValue === 300102" class="mb-4">
|
||||
<FormInput
|
||||
title="비밀번호"
|
||||
@ -50,6 +56,8 @@
|
||||
:is-essential="true"
|
||||
:is-alert="passwordAlert"
|
||||
v-model="password"
|
||||
@update:alert="passwordAlert = $event"
|
||||
@input="validatePassword"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -61,14 +69,16 @@
|
||||
@update:isValid="isFileValid = $event"
|
||||
/>
|
||||
|
||||
<!-- 내용 입력 (에디터) -->
|
||||
<div class="mb-4">
|
||||
<label for="html5-tel-input" class="col-md-2 col-form-label">
|
||||
내용
|
||||
<span class="text-danger">*</span>
|
||||
<div class="invalid-feedback" :class="contentAlert ? 'display-block' : ''">내용을 확인해주세요.</div>
|
||||
<label class="col-md-2 col-form-label">
|
||||
내용 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="col-md-12">
|
||||
<QEditor @update:data="content = $event" />
|
||||
<QEditor @update:data="updateContent" />
|
||||
</div>
|
||||
<div class="invalid-feedback mt-1" :class="contentAlert ? 'd-block' : 'd-none'">
|
||||
내용을 입력해주세요.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -115,10 +125,9 @@ const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await axios.get('board/categories');
|
||||
categoryList.value = response.data.data;
|
||||
// "자유" 카테고리 찾기 (CMNCODNAM이 '자유'인 것 선택)
|
||||
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
|
||||
if (freeCategory) {
|
||||
categoryValue.value = freeCategory.CMNCODVAL; // 기본 선택값 설정
|
||||
categoryValue.value = freeCategory.CMNCODVAL;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('카테고리 불러오기 오류:', error);
|
||||
@ -129,15 +138,44 @@ 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 () => {
|
||||
titleAlert.value = !title.value;
|
||||
passwordAlert.value = categoryValue.value === 300102 && !password.value;
|
||||
contentAlert.value = !content.value;
|
||||
validateTitle();
|
||||
validatePassword();
|
||||
|
||||
categoryAlert.value = !categoryValue.value;
|
||||
contentAlert.value = content.value.length === 0;
|
||||
|
||||
if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
|
||||
return;
|
||||
@ -154,26 +192,6 @@ const write = async () => {
|
||||
const { data: boardResponse } = await axios.post('board', boardData);
|
||||
const boardId = boardResponse.data;
|
||||
|
||||
if (attachFiles.value && attachFiles.value.length > 0) {
|
||||
for (const file of attachFiles.value) {
|
||||
const formData = new FormData();
|
||||
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
|
||||
|
||||
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: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toastStore.onToast('게시물이 작성되었습니다.', 's');
|
||||
goList();
|
||||
} catch (error) {
|
||||
|
||||