Merge remote-tracking branch 'origin/main' into board-comment-2

This commit is contained in:
kimdaae328 2025-02-28 14:07:19 +09:00
commit c68e8839b3
21 changed files with 116 additions and 54 deletions

BIN
public/img/mbti/enfj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
public/img/mbti/enfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/entj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
public/img/mbti/entp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/img/mbti/esfj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/img/mbti/esfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/mbti/est.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/img/mbti/estp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/mbti/infj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/infp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/mbti/intj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
public/img/mbti/intp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/isfj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
public/img/mbti/isfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/mbti/istj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
public/img/mbti/istp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -15,6 +15,7 @@
:disabled="disabled" :disabled="disabled"
:min="min" :min="min"
@focusout="$emit('focusout', modelValue)" @focusout="$emit('focusout', modelValue)"
@input="handleInput"
/> />
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''"> <div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">
{{ title }} 확인해주세요. {{ title }} 확인해주세요.
@ -92,11 +93,6 @@ const inputValue = ref(props.modelValue);
// //
watch(inputValue, (newValue) => { watch(inputValue, (newValue) => {
emits('update:modelValue', 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> </script>

View File

@ -4,19 +4,30 @@
{{ 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'" class="d-flex gap-2 align-items-center">
<select class="form-select" :id="name" v-model="selectData" :disabled="disabled"> <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"> <option v-for="(item, i) in data" :key="i" :value="isCommon ? item.value : i" :style="isColor ? { color: item.label } : {}">
{{ isCommon ? item.label : item }} {{ isCommon ? item.label : item }}
</option> </option>
</select> </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>
<div v-if="isAlert" class="invalid-feedback">{{ title }} 확인해주세요.</div> <div v-if="isAlert" class="invalid-feedback">{{ title }} 확인해주세요.</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
const props = defineProps({ const props = defineProps({
title: { title: {
@ -67,7 +78,17 @@ const props = defineProps({
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false default: false
} },
isColor: {
type: Boolean,
default: false,
required: false,
},
isMbti: {
type: Boolean,
default: false,
required: false,
},
}); });
const emit = defineEmits(['update:data']); const emit = defineEmits(['update:data']);
@ -93,4 +114,13 @@ watch(() => props.data, (newData) => {
watch(selectData, (newValue) => { watch(selectData, (newValue) => {
emit('update:data', 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> </script>

View File

@ -68,20 +68,21 @@
:is-alert="passwordcheckAlert" :is-alert="passwordcheckAlert"
@update:data="passwordcheck = $event" @update:data="passwordcheck = $event"
@update:alert="passwordcheckAlert = $event" @update:alert="passwordcheckAlert = $event"
@blur="checkPw" @input="checkPw"
:value="passwordcheck" :value="passwordcheck"
/> />
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span> <span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
<div class="d-grid gap-2 mt-5 mb-5"> <div class="d-flex gap-2 mt-7 mb-3">
<SaveBtn @click="handleNewPassword" /> <BackBtn class=" w-50" @click="handleback"/>
<p v-if="pwErrMsg" class="invalid-feedback d-block mb-0">{{ pwErrMsg }}</p> <SaveBtn class="w-50" @click="handleNewPassword" />
</div> </div>
<p v-if="pwErrMsg" class="invalid-feedback d-block mb-0">{{ pwErrMsg }}</p>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref, watch } from 'vue';
import $api from '@api'; import $api from '@api';
import commonApi from '@/common/commonApi'; import commonApi from '@/common/commonApi';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -112,7 +113,7 @@
const passwordAlert = ref(false); const passwordAlert = ref(false);
const passwordcheckAlert = ref(false); const passwordcheckAlert = ref(false);
const passwordcheckErrorAlert = ref(false); const passwordMismatch = ref(false);
const { pwhintList } = commonApi({ const { pwhintList } = commonApi({
loadPwhint: true, loadPwhint: true,
@ -127,6 +128,13 @@
router.push('/login'); router.push('/login');
} }
//
watch([password, passwordcheck], () => {
if (passwordcheck.value !== '') {
checkPw();
}
});
// , , , member input // , , , member input
const handleSubmit = async () => { const handleSubmit = async () => {
userCheckMsg.value = ''; userCheckMsg.value = '';
@ -153,13 +161,13 @@
} }
}; };
const checkPw = async () => { const checkPw = () => {
if (password.value !== passwordcheck.value) { if (password.value !== passwordcheck.value) {
passwordcheckError.value = '비밀번호가 일치하지 않습니다.'; passwordcheckError.value = '비밀번호가 일치하지 않습니다.';
passwordcheckErrorAlert.value = true; passwordMismatch.value = true;
} else { } else {
passwordcheckError.value = ''; passwordcheckError.value = '';
passwordcheckErrorAlert.value = false; passwordMismatch.value = false;
} }
}; };
@ -168,8 +176,10 @@
pwErrMsg.value = ''; pwErrMsg.value = '';
passwordAlert.value = password.value.trim() === ''; passwordAlert.value = password.value.trim() === '';
passwordcheckAlert.value = passwordcheck.value.trim() === ''; passwordcheckAlert.value = passwordcheck.value.trim() === '';
checkPw(); checkPw();
if (passwordAlert.value || passwordcheckAlert.value || passwordcheckErrorAlert.value) {
if (passwordAlert.value || passwordcheckAlert.value || passwordMismatch.value) {
return; return;
} }
@ -192,6 +202,5 @@
toastStore.onToast('비밀번호가 재설정 되었습니다.', 's'); toastStore.onToast('비밀번호가 재설정 되었습니다.', 's');
router.push('/login'); router.push('/login');
} }
}; };
</script> </script>

View File

@ -92,6 +92,7 @@
:is-row="false" :is-row="false"
:is-label="true" :is-label="true"
:is-common="true" :is-common="true"
:is-color="true"
:data="colorList" :data="colorList"
@update:data="color = $event" @update:data="color = $event"
class="w-50" class="w-50"
@ -118,6 +119,7 @@
:is-row="false" :is-row="false"
:is-label="true" :is-label="true"
:is-common="true" :is-common="true"
:is-mbti="true"
:data="mbtiList" :data="mbtiList"
@update:data="mbti = $event" @update:data="mbti = $event"
class="w-50" class="w-50"

View File

@ -9,12 +9,15 @@
<div class="col-xl-12"> <div class="col-xl-12">
<div class="card-body"> <div class="card-body">
<!-- 제목 입력 -->
<FormInput <FormInput
title="제목" title="제목"
name="title" name="title"
:is-essential="true" :is-essential="true"
:is-alert="titleAlert" :is-alert="titleAlert"
v-model="title" v-model="title"
@update:alert="titleAlert = $event"
@input="validateTitle"
/> />
<!-- 카테고리 선택 --> <!-- 카테고리 선택 -->
@ -32,16 +35,19 @@
:id="`category-${index}`" :id="`category-${index}`"
:value="category.CMNCODVAL" :value="category.CMNCODVAL"
v-model="categoryValue" v-model="categoryValue"
@change="categoryAlert = false"
/> />
<label class="form-check-label" :for="`category-${index}`"> <label class="form-check-label" :for="`category-${index}`">
{{ category.CMNCODNAM }} {{ category.CMNCODNAM }}
</label> </label>
</div> </div>
</div> </div>
<div class="invalid-feedback" :class="categoryAlert ? 'display-block' : ''">카테고리를 선택해주세요.</div> <div class="invalid-feedback" :class="categoryAlert ? 'd-block' : 'd-none'">
카테고리를 선택해주세요.
</div>
</div> </div>
<!-- 비밀번호 필드 --> <!-- 비밀번호 필드 (익명게시판 선택 활성화) -->
<div v-if="categoryValue === 300102" class="mb-4"> <div v-if="categoryValue === 300102" class="mb-4">
<FormInput <FormInput
title="비밀번호" title="비밀번호"
@ -50,6 +56,8 @@
:is-essential="true" :is-essential="true"
:is-alert="passwordAlert" :is-alert="passwordAlert"
v-model="password" v-model="password"
@update:alert="passwordAlert = $event"
@input="validatePassword"
/> />
</div> </div>
@ -61,14 +69,16 @@
@update:isValid="isFileValid = $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 class="col-md-2 col-form-label">
내용 내용 <span class="text-danger">*</span>
<span class="text-danger">*</span>
<div class="invalid-feedback" :class="contentAlert ? 'display-block' : ''">내용을 확인해주세요.</div>
</label> </label>
<div class="col-md-12"> <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>
</div> </div>
@ -115,10 +125,9 @@ const fetchCategories = async () => {
try { try {
const response = await axios.get('board/categories'); const response = await axios.get('board/categories');
categoryList.value = response.data.data; categoryList.value = response.data.data;
// "" (CMNCODNAM '' )
const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유'); const freeCategory = categoryList.value.find(category => category.CMNCODNAM === '자유');
if (freeCategory) { if (freeCategory) {
categoryValue.value = freeCategory.CMNCODVAL; // categoryValue.value = freeCategory.CMNCODVAL;
} }
} catch (error) { } catch (error) {
console.error('카테고리 불러오기 오류:', error); console.error('카테고리 불러오기 오류:', error);
@ -129,15 +138,44 @@ onMounted(() => {
fetchCategories(); 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 = () => { const goList = () => {
router.push('/board'); router.push('/board');
}; };
const write = async () => { const write = async () => {
titleAlert.value = !title.value; validateTitle();
passwordAlert.value = categoryValue.value === 300102 && !password.value; validatePassword();
contentAlert.value = !content.value;
categoryAlert.value = !categoryValue.value; categoryAlert.value = !categoryValue.value;
contentAlert.value = content.value.length === 0;
if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) { if (titleAlert.value || passwordAlert.value || contentAlert.value || categoryAlert.value || !isFileValid.value) {
return; return;
@ -154,26 +192,6 @@ const write = async () => {
const { data: boardResponse } = await axios.post('board', boardData); const { data: boardResponse } = await axios.post('board', boardData);
const boardId = boardResponse.data; 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'); toastStore.onToast('게시물이 작성되었습니다.', 's');
goList(); goList();
} catch (error) { } catch (error) {