수정사항

This commit is contained in:
dyhj625 2025-04-07 10:34:49 +09:00
parent 44cec4cccd
commit eb39a2a0b7
6 changed files with 196 additions and 156 deletions

View File

@ -812,3 +812,9 @@ input:checked + .slider:before {
.mr-1{ .mr-1{
margin-right: 0.25rem !important; margin-right: 0.25rem !important;
} }
.nickname-ellipsis {
white-space: nowrap;
max-width: 100px;
vertical-align: middle;
}

View File

@ -41,6 +41,8 @@
v-model="nickname" v-model="nickname"
placeholder="닉네임" placeholder="닉네임"
@input="clearAlert('nickname')" @input="clearAlert('nickname')"
@keypress="noSpace"
:maxlength="6"
/> />
<!-- 닉네임 경고 메시지 --> <!-- 닉네임 경고 메시지 -->
<div v-if="nicknameAlert" class="position-absolute text-danger small top-100 start-0"> <div v-if="nicknameAlert" class="position-absolute text-danger small top-100 start-0">
@ -109,6 +111,10 @@
}, },
}); });
const noSpace = (e) => {
if (e.key === ' ') e.preventDefault();
};
const $common = inject('common'); const $common = inject('common');
const comment = ref(''); const comment = ref('');
const password = ref(''); const password = ref('');

View File

@ -44,11 +44,17 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<!-- 익명게시판은 'nickname', 나머지는 'writer' --> <!-- 익명게시판은 '닉네임', 나머지는 '작성자' -->
<th class="text-center" style="width: 20%;"> <th class="text-start">
<div class="ms-4">
{{ selectedBoard === 'anonymous' ? '닉네임' : '작성자' }} {{ selectedBoard === 'anonymous' ? '닉네임' : '작성자' }}
</div>
</th>
<th class="text-start" style="width: 65%;">
<div class="ms-4">
제목
</div>
</th> </th>
<th class="text-center" style="width: 65%;">제목</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -58,11 +64,13 @@
style="cursor: pointer;" style="cursor: pointer;"
@click="goDetail(post.id, selectedBoard)" @click="goDetail(post.id, selectedBoard)"
> >
<td class="text-center small"> <td class="text-start nickname-ellipsis small">
<div class="ms-4">
{{ selectedBoard === 'anonymous' ? post.nickname : post.author }} {{ selectedBoard === 'anonymous' ? post.nickname : post.author }}
</div>
</td> </td>
<td class="text-start fs-6"> <td class="text-start fs-6">
<div class="ms-2"> <div class="ms-4">
{{ truncateTitle(post.title) }} {{ truncateTitle(post.title) }}
<span v-if="post.commentCount" class="text-danger ml-1 small"> <span v-if="post.commentCount" class="text-danger ml-1 small">
[{{ post.commentCount }}] [{{ post.commentCount }}]
@ -120,97 +128,97 @@ const anonymousList = ref([]);
// computed // computed
const currentList = computed(() => { const currentList = computed(() => {
if (selectedBoard.value === 'notices') return noticeList.value; if (selectedBoard.value === 'notices') return noticeList.value;
if (selectedBoard.value === 'general') return freeList.value; if (selectedBoard.value === 'general') return freeList.value;
if (selectedBoard.value === 'anonymous') return anonymousList.value; if (selectedBoard.value === 'anonymous') return anonymousList.value;
return []; return [];
}); });
// : HH:mm, YYYY-MM-DD // : HH:mm, YYYY-MM-DD
const formatDate = dateString => { const formatDate = dateString => {
const date = dayjs(dateString); const date = dayjs(dateString);
return date.isToday() ? date.format('HH:mm') : date.format('YYYY-MM-DD'); return date.isToday() ? date.format('HH:mm') : date.format('YYYY-MM-DD');
}; };
// 14 ... // 14 ...
const truncateTitle = title => { const truncateTitle = title => {
return title.length > 7 ? title.slice(0, 7) + '...' : title; return title.length > 7 ? title.slice(0, 7) + '...' : title;
}; };
// ( 5) // ( 5)
const fetchNoticePosts = async () => { const fetchNoticePosts = async () => {
try { try {
const { data } = await axios.get('board/notices', { params: { size: 8 } }); const { data } = await axios.get('board/notices', { params: { size: 8 } });
if (data?.data) { if (data?.data) {
noticeList.value = data.data.map(post => ({ noticeList.value = data.data.map(post => ({
id: post.id, id: post.id,
title: post.title, title: post.title,
date: formatDate(post.date), date: formatDate(post.date),
rawDate: post.date, rawDate: post.date,
views: post.cnt || 0, views: post.cnt || 0,
commentCount: post.commentCount, commentCount: post.commentCount,
img: post.firstImageUrl, img: post.firstImageUrl,
author: post.author || '관리자', author: post.author || '관리자',
nickname: post.nickname || '관리자', nickname: post.nickname || '관리자',
hasAttachment: post.hasAttachment, // hasAttachment: post.hasAttachment, //
})); }));
}
} catch (error) {
} }
} catch (error) {
}
}; };
// board/general ( 10 5) // board/general ( 10 5)
const fetchGeneralPosts = async () => { const fetchGeneralPosts = async () => {
try { try {
const { data } = await axios.get('board/general', { params: { size: 16 } }); const { data } = await axios.get('board/general', { params: { size: 16 } });
if (data?.data && data.data.list) { if (data?.data && data.data.list) {
const freePosts = []; const freePosts = [];
const anonymousPosts = []; const anonymousPosts = [];
data.data.list.forEach(post => { data.data.list.forEach(post => {
if (post.nickname) { if (post.nickname) {
// //
anonymousPosts.push({ anonymousPosts.push({
id: post.id, id: post.id,
title: post.title, title: post.title,
date: formatDate(post.date), date: formatDate(post.date),
img: post.firstImageUrl, img: post.firstImageUrl,
rawDate: post.date, rawDate: post.date,
views: post.cnt || 0, views: post.cnt || 0,
commentCount: post.commentCount, commentCount: post.commentCount,
nickname: post.nickname, nickname: post.nickname,
hasAttachment: post.hasAttachment, // hasAttachment: post.hasAttachment, //
}); });
} else { } else {
// //
freePosts.push({ freePosts.push({
id: post.id, id: post.id,
title: post.title, title: post.title,
date: formatDate(post.date), date: formatDate(post.date),
rawDate: post.date, rawDate: post.date,
views: post.cnt || 0, views: post.cnt || 0,
img: post.firstImageUrl, img: post.firstImageUrl,
commentCount: post.commentCount, commentCount: post.commentCount,
author: post.author || '익명', author: post.author || '익명',
hasAttachment: post.hasAttachment, // hasAttachment: post.hasAttachment, //
});
}
}); });
freeList.value = freePosts.slice(0, 8);
anonymousList.value = anonymousPosts.slice(0, 8);
} }
}); } catch (error) {
freeList.value = freePosts.slice(0, 8); console.error(error);
anonymousList.value = anonymousPosts.slice(0, 8);
} }
} catch (error) {
console.error(error);
}
}; };
// //
const changeBoard = type => { const changeBoard = type => {
selectedBoard.value = type; selectedBoard.value = type;
}; };
// ( ) // ( )
const goDetail = (id, boardType) => { const goDetail = (id, boardType) => {
router.push({ name: 'BoardDetail', params: { id }, query: { type: boardType } }); router.push({ name: 'BoardDetail', params: { id }, query: { type: boardType } });
}; };
// //
@ -220,6 +228,6 @@ fetchGeneralPosts();
<style scoped> <style scoped>
.table > :not(caption) > * > * { .table > :not(caption) > * > * {
padding: 0 !important; padding: 0 !important;
} }
</style> </style>

View File

@ -80,7 +80,7 @@
<span v-if="isNewPost(notice.rawDate)" class="box-new badge text-white ms-2 fs-tiny"> N </span> <span v-if="isNewPost(notice.rawDate)" class="box-new badge text-white ms-2 fs-tiny"> N </span>
</div> </div>
</td> </td>
<td class="text-center">{{ notice.author }}</td> <td class="text-start">{{ notice.author }}</td>
<td class="text-center">{{ notice.date }}</td> <td class="text-center">{{ notice.date }}</td>
<td class="text-center">{{ notice.views }}</td> <td class="text-center">{{ notice.views }}</td>
</tr> </tr>
@ -105,7 +105,7 @@
<span v-if="isNewPost(post.rawDate)" class="box-new badge text-white ms-2 fs-tiny">N</span> <span v-if="isNewPost(post.rawDate)" class="box-new badge text-white ms-2 fs-tiny">N</span>
</div> </div>
</td> </td>
<td class="text-center">{{ post.nickname ? post.nickname : post.author }}</td> <td class="text-start nickname-ellipsis">{{ post.nickname ? post.nickname : post.author }}</td>
<td class="text-center">{{ post.date }}</td> <td class="text-center">{{ post.date }}</td>
<td class="text-center">{{ post.views }}</td> <td class="text-center">{{ post.views }}</td>
</tr> </tr>
@ -384,4 +384,5 @@
position: relative; position: relative;
top: -1px; top: -1px;
} }
</style> </style>

View File

@ -50,6 +50,8 @@
v-model="nickname" v-model="nickname"
@update:alert="nicknameAlert = $event" @update:alert="nicknameAlert = $event"
@input="validateNickname" @input="validateNickname"
@keypress="noSpace"
:maxlength="6"
/> />
<FormInput <FormInput
title="비밀번호" title="비밀번호"
@ -146,6 +148,10 @@
const editorUploadedImgList = ref([]); const editorUploadedImgList = ref([]);
const editorDeleteImgList = ref([]); const editorDeleteImgList = ref([]);
const noSpace = (e) => {
if (e.key === ' ') e.preventDefault();
};
const fetchCategories = async () => { const fetchCategories = async () => {
const response = await axios.get('board/categories'); const response = await axios.get('board/categories');
categoryList.value = response.data.data; categoryList.value = response.data.data;
@ -206,7 +212,8 @@
const validateNickname = () => { const validateNickname = () => {
if (categoryValue.value === 300102) { if (categoryValue.value === 300102) {
nicknameAlert.value = nickname.value.trim().length === 0; nickname.value = nickname.value.replace(/\s/g, ''); //
nicknameAlert.value = nickname.value.length === 0 ;
} else { } else {
nicknameAlert.value = false; nicknameAlert.value = false;
} }

View File

@ -55,8 +55,11 @@
<ArrInput title="주소" name="address" v-model="form.address" :disabled="true" /> <ArrInput title="주소" name="address" v-model="form.address" :disabled="true" />
<UserFormInput title="전화번호" name="phone" :value="form.phone" <UserFormInput title="전화번호" name="phone" :value="form.phone"
@update:data="form.phone = $event" @blur="checkPhoneDuplicate" @update:data="form.phone = $event" @blur="checkPhoneDuplicateAndFormat"
:maxlength="11" @keypress="onlyNumber" /> :maxlength="11" @keypress="onlyNumber" />
<span v-if="phoneFormatError" class="text-danger invalid-feedback mt-1 d-block">
전화번호 형식이 올바르지 않습니다.
</span>
<span v-if="phoneDuplicated" class="text-danger invalid-feedback mt-1 d-block"> <span v-if="phoneDuplicated" class="text-danger invalid-feedback mt-1 d-block">
이미 사용 중인 전화번호입니다. 이미 사용 중인 전화번호입니다.
</span> </span>
@ -142,6 +145,7 @@ const colorList = ref([]);
const password = ref({ current: '', new: '', confirm: '' }); const password = ref({ current: '', new: '', confirm: '' });
const passwordError = ref(false); const passwordError = ref(false);
const phoneFormatError = ref(false);
const showResetPw = ref(false); const showResetPw = ref(false);
const canResetPassword = computed(() => { const canResetPassword = computed(() => {
@ -162,93 +166,101 @@ watch(
); );
const isChanged = computed(() => { const isChanged = computed(() => {
const f = form.value; const f = form.value;
const o = originalData.value; const o = originalData.value;
return ( return (
f.entryDate !== o.entryDate || f.birth !== o.birth || f.phone !== o.phone || f.entryDate !== o.entryDate || f.birth !== o.birth || f.phone !== o.phone ||
f.color !== o.color || f.mbti !== o.mbti || profileChanged.value || f.color !== o.color || f.mbti !== o.mbti || profileChanged.value ||
f.address.address !== o.address.address || f.address.address !== o.address.address ||
f.address.detailAddress !== o.address.detailAddress || f.address.detailAddress !== o.address.detailAddress ||
f.address.postcode !== o.address.postcode f.address.postcode !== o.address.postcode
); );
}); });
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, ''); const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
const defaultProfile = "img/avatars/default-Profile.jpg"; const defaultProfile = "/img/icons/icon.png";
const getProfileImageUrl = (fileName) => const getProfileImageUrl = (profilePath) =>
fileName?.trim() ? `${baseUrl}upload/img/profile/${fileName}?t=${Date.now()}` : defaultProfile; profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
const profilePreviewStyle = computed(() => ({ const profilePreviewStyle = computed(() => ({
width: '100px', width: '100px',
height: '100px', height: '100px',
backgroundImage: `url(${profile.value})`, backgroundImage: `url(${profile.value})`,
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center' backgroundPosition: 'center'
})); }));
const profileUpload = (e) => { const profileUpload = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
if (file.size > 5 * 1024 * 1024 || !['image/jpeg', 'image/png'].includes(file.type)) { if (file.size > 5 * 1024 * 1024 || !['image/jpeg', 'image/png'].includes(file.type)) {
profilerr.value = '5MB 이하의 JPG/PNG 파일만 업로드 가능합니다.'; profilerr.value = '5MB 이하의 JPG/PNG 파일만 업로드 가능합니다.';
return; return;
} }
profilerr.value = ''; profilerr.value = '';
if (currentBlobUrl.value) URL.revokeObjectURL(currentBlobUrl.value); if (currentBlobUrl.value) URL.revokeObjectURL(currentBlobUrl.value);
uploadedFile.value = file; uploadedFile.value = file;
const newBlobUrl = URL.createObjectURL(file); const newBlobUrl = URL.createObjectURL(file);
profile.value = newBlobUrl; profile.value = newBlobUrl;
currentBlobUrl.value = newBlobUrl; currentBlobUrl.value = newBlobUrl;
profileChanged.value = true; profileChanged.value = true;
}; };
const onlyNumber = (e) => { const onlyNumber = (e) => {
if (!/[0-9]/.test(e.key)) e.preventDefault(); if (!/[0-9]/.test(e.key)) e.preventDefault();
}; };
const checkPhoneDuplicate = async () => { const checkPhoneDuplicateAndFormat = async () => {
const currentPhone = form.value.phone; const phone = form.value.phone.trim();
phoneDuplicated.value = currentPhone !== originalData.value.phone &&
!(await $api.get('/user/checkPhone', { params: { memberTel: currentPhone } })).data.data; //
const phoneRegex = /^010\d{8}$/;
phoneFormatError.value = !phoneRegex.test(phone);
if (!phoneFormatError.value) {
phoneDuplicated.value = phone !== originalData.value.phone &&
!(await $api.get('/user/checkPhone', {
params: { memberTel: currentPhone },
})).data.data;
}
}; };
const handleColorUpdate = async (colorVal) => { const handleColorUpdate = async (colorVal) => {
form.value.color = colorVal; form.value.color = colorVal;
colorDuplicated.value = colorVal !== originalData.value.color && colorDuplicated.value = colorVal !== originalData.value.color &&
(await $api.get('/user/checkColor', { params: { memberCol: colorVal } })).data.data; (await $api.get('/user/checkColor', { params: { memberCol: colorVal } })).data.data;
}; };
const checkCurrentPassword = async () => { const checkCurrentPassword = async () => {
if (!password.value.current) return; if (!password.value.current) return;
const res = await $api.post('/user/checkPassword', { const res = await $api.post('/user/checkPassword', {
id: form.value.id, id: form.value.id,
password: password.value.current password: password.value.current
}); });
passwordError.value = res.data.data; passwordError.value = res.data.data;
showResetPw.value = !res.data.data; showResetPw.value = !res.data.data;
}; };
const handlePasswordReset = async () => { const handlePasswordReset = async () => {
const res = await $api.patch('/user/pwNew', { const res = await $api.patch('/user/pwNew', {
id: form.value.id, id: form.value.id,
password: password.value.new password: password.value.new
}); });
if (res.data.data) { if (res.data.data) {
toastStore.onToast('비밀번호가 변경되었습니다.', 's'); toastStore.onToast('비밀번호가 변경되었습니다.', 's');
password.value = { current: '', new: '', confirm: '' }; password.value = { current: '', new: '', confirm: '' };
showResetPw.value = false; showResetPw.value = false;
passwordError.value = false; passwordError.value = false;
} else { } else {
toastStore.onToast('비밀번호 변경 실패', 'e'); toastStore.onToast('비밀번호 변경 실패', 'e');
} }
}; };
const formatDate = (isoDate) => isoDate?.split('T')[0] || ''; const formatDate = (isoDate) => isoDate?.split('T')[0] || '';
const loadInitialData = async () => { const loadInitialData = async () => {
const user = (await $api.get('/user/userInfo')).data.data; const user = (await $api.get('/user/userInfo')).data.data;
const serverColors = (await $api.get('/user/color', { params: { type: 'YON' } })).data.data.map(c => ({ const serverColors = (await $api.get('/user/color', { params: { type: 'YON' } })).data.data.map(c => ({
value: c.CMNCODVAL, label: c.CMNCODNAM value: c.CMNCODVAL, label: c.CMNCODNAM
})); }));
const matchedColor = serverColors.find(c => c.label === user.usercolor); const matchedColor = serverColors.find(c => c.label === user.usercolor);
const colorCode = matchedColor ? matchedColor.value : user.color; const colorCode = matchedColor ? matchedColor.value : user.color;
@ -275,32 +287,32 @@ profile.value = getProfileImageUrl(user.profile);
profileChanged.value = false; profileChanged.value = false;
const mbtiRes = await $api.get('/user/mbti'); const mbtiRes = await $api.get('/user/mbti');
mbtiList.value = mbtiRes.data.data.map(m => ({ value: m.CMNCODVAL, label: m.CMNCODNAM })); mbtiList.value = mbtiRes.data.data.map(m => ({ value: m.CMNCODVAL, label: m.CMNCODNAM }));
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
const formData = new FormData(); const formData = new FormData();
Object.entries(form.value).forEach(([k, v]) => { Object.entries(form.value).forEach(([k, v]) => {
if (typeof v === 'object') { if (typeof v === 'object') {
formData.append('address', v.address); formData.append('address', v.address);
formData.append('detailAddress', v.detailAddress); formData.append('detailAddress', v.detailAddress);
formData.append('postcode', v.postcode); formData.append('postcode', v.postcode);
} else { } else {
formData.append(k, v); formData.append(k, v);
}
});
if (uploadedFile.value) formData.append('profileFile', uploadedFile.value);
if (form.value.color !== originalData.value.color) {
if (form.value.color) await $api.patch('/user/updateColorYon', { color: form.value.color, type: 'YON' });
if (originalData.value.color) await $api.patch('/user/updateColorChange', { color: originalData.value.color, type: 'YON' });
} }
});
if (uploadedFile.value) formData.append('profileFile', uploadedFile.value);
if (form.value.color !== originalData.value.color) { await $api.patch('/user/updateInfo', formData, { isFormData: true });
if (form.value.color) await $api.patch('/user/updateColorYon', { color: form.value.color, type: 'YON' }); originalData.value = { ...form.value };
if (originalData.value.color) await $api.patch('/user/updateColorChange', { color: originalData.value.color, type: 'YON' }); profileChanged.value = false;
} location.reload();
toastStore.onToast('정보가 수정되었습니다.', 's');
await $api.patch('/user/updateInfo', formData, { isFormData: true });
originalData.value = { ...form.value };
profileChanged.value = false;
location.reload();
toastStore.onToast('정보가 수정되었습니다.', 's');
}; };
onMounted(() => loadInitialData()); onMounted(() => loadInitialData());