Merge branch 'main' into khj

This commit is contained in:
yoon 2025-01-23 12:43:26 +09:00
commit 5cab531cdd
10 changed files with 322 additions and 146 deletions

View File

@ -2,7 +2,7 @@
<div class="mb-2 row">
<label :for="name" class="col-md-2 col-form-label" :class="isLabel ? 'd-block' : 'd-none'">
{{ title }}
<span class="text-danger">*</span>
<span :class="isEssential ? 'text-red' : 'none'">*</span>
</label>
<div class="col-md-10">
<input

View File

@ -1,12 +1,14 @@
<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'">
{{ title }}
<span :class="isEssential ? 'link-danger' : 'none'">*</span>
</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">
<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>
</div>
<div v-if="isAlert" class="invalid-feedback">{{ title }} 확인해주세요.</div>
@ -56,13 +58,24 @@ const props = defineProps({
type: Boolean,
default: true,
required: false,
},
isCommon : {
type: Boolean,
default: false,
required: false,
}
});
const emit = defineEmits(['update:data']);
const selectData = ref(props.value);
watchEffect(() => {
if (props.isCommon && props.data.length > 0) {
selectData.value = props.data[0].value; //
} else {
selectData.value = props.value; //
}
emit('update:data', selectData.value);
})
</script>

View File

@ -11,9 +11,10 @@
class="form-control"
:type="type"
@input="updateInput"
:value="value"
:value="computedValue"
:maxLength="maxlength"
:placeholder="title"
@blur="$emit('blur')"
/>
<span class="input-group-text">@ localhost.co.kr</span>
</div>
@ -23,78 +24,90 @@
class="form-control"
:type="type"
@input="updateInput"
:value="value"
:value="computedValue"
:maxLength="maxlength"
:placeholder="title"
@blur="$emit('blur')"
/>
</div>
<div class="invalid-feedback" :class="isAlert ? 'd-block' : ''">{{ title }} 확인해주세요.</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { inject, computed } from 'vue';
const prop = defineProps({
title: {
type: String,
default: '라벨',
required: true,
},
name: {
type: String,
default: 'nameplz',
required: true,
},
isEssential: {
type: Boolean,
default: false,
required: false,
},
type: {
type: String,
default: 'text',
required: false,
},
value: {
type: String,
default: '',
require: false,
},
maxlength: {
type: Number,
default: 30,
required: false,
},
isAlert : {
type: Boolean,
default: false,
required: false,
},
useInputGroup: {
type: Boolean,
default: false,
required: false,
},
});
const props = defineProps({
title: {
type: String,
default: '라벨',
required: true,
},
name: {
type: String,
default: 'nameplz',
required: true,
},
isEssential: {
type: Boolean,
default: false,
required: false,
},
type: {
type: String,
default: 'text',
required: false,
},
value: {
type: String,
default: '',
required: false,
},
maxlength: {
type: Number,
default: 30,
required: false,
},
isAlert: {
type: Boolean,
default: false,
required: false,
},
useInputGroup: {
type: Boolean,
default: false,
required: false,
},
});
const emits = defineEmits(['update:data', 'update:alert'])
const emits = defineEmits(['update:data', 'update:alert', 'blur']);
const updateInput = function (event) {
//Type Number maxlength
if (event.target.value.length > prop.maxlength) {
event.target.value = event.target.value.slice(0, prop.maxlength);
}
emits('update:data', event.target.value);
// dayjs
const dayjs = inject('dayjs');
// isAlert false
if (event.target.value.trim() !== '') { emits('update:alert', false); }
// 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);
}
// isAlert false
if (newValue.trim() !== '') {
emits('update:alert', false);
}
};
</script>
<style>
</style>

View File

@ -34,11 +34,11 @@
<span class="text-muted me-3">
<i class="fa-regular fa-eye"></i> {{ views || 0 }}
</span>
<span class="text-muted me-3">
<i class="bx bx-like"></i> {{ likes || 0 }}
<span class="text-muted me-3" v-if="likes != null">
<i class="bx bx-like"></i> {{ likes }}
</span>
<span class="text-muted">
<i class="bx bx-comment"></i> {{ comments || 0 }}
<span class="text-muted" v-if="comments !== null">
<i class="bx bx-comment"></i> {{ comments }}
</span>
</div>
</div>
@ -80,11 +80,11 @@ const props = defineProps({
},
likes: {
type: Number,
default: 0,
default: null,
},
comments: {
type: Number,
default: 0,
default: null,
},
attachment: {
type: Boolean,

View File

@ -1,43 +1,53 @@
<template>
<div class="mt-4">
<div v-if="posts.length === 0" class="text-center">
게시물이 없습니다.
<p class="text-muted mt-4">게시물이 없습니다.</p>
</div>
<div v-for="post in posts" :key="post.id" @click="handleClick(post.id)">
<BoardCard
:img="post.img"
:category="post.category"
:img="post.img || null"
:category="post.category || ''"
:title="post.title"
:content="post.content"
:date="post.date"
:views="post.views"
:likes="post.likes"
:comments="post.comments"
:attachment="post.attachment"
:views="post.views || 0"
v-bind="getBoardCardProps(post)"
:attachment="post.attachment || false"
@click="() => goDetail(post.id)"
/>
</div>
</div>
</template>
<script>
<script setup>
import { defineProps, defineEmits } from 'vue';
import BoardCard from './BoardCard.vue';
export default {
components: {
BoardCard,
},
props: {
posts: {
type: Array,
required: true,
},
},
emits: ['click'],
methods: {
handleClick(id) {
this.$emit('click', id);
},
},
const props = defineProps({
posts: {
type: Array,
required: true,
default: () => [],
}
});
const emit = defineEmits(['click']);
//
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>

View File

@ -1,6 +1,6 @@
<template>
<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>
</div>
</template>

View File

@ -12,7 +12,7 @@
<UserFormInput
title="비밀번호"
name="pw"
name="password"
type="password"
:is-alert="passwordAlert"
@update:data="handlePasswordChange"
@ -36,6 +36,8 @@
</template>
<script setup>
import $api from '@api';
import router from '@/router';
import { ref } from 'vue';
import UserFormInput from '@c/input/UserFormInput.vue';
@ -44,8 +46,6 @@
const idAlert = ref(false);
const passwordAlert = ref(false);
const emit = defineEmits(['submit']);
const handleIdChange = value => {
id.value = value;
idAlert.value = false;
@ -56,12 +56,23 @@
passwordAlert.value = false;
};
const handleSubmit = () => {
idAlert.value = id.value.trim() === '';
passwordAlert.value = password.value.trim() === '';
const handleSubmit = async () => {
if (!idAlert.value && !passwordAlert.value) {
emit('submit', { id: id.value, password: password.value });
try {
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>

View File

@ -3,11 +3,12 @@
<div class="text-center">
<label
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"
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>
<span class="link-danger position-absolute">*</span>
<input type="file" id="profilePic" class="d-none" @change="profileUpload" />
<span v-if="profilerr" class="invalid-feedback d-block">{{ profilerr }}</span>
@ -22,8 +23,10 @@
:useInputGroup="true"
@update:data="id = $event"
@update:alert="idAlert = $event"
@blur="checkIdDuplicate"
:value="id"
/>
<span v-if="idError" class="invalid-feedback d-block">{{ idError }}</span>
<UserFormInput
title="비밀번호"
@ -44,16 +47,28 @@
:is-alert="passwordcheckAlert"
@update:data="passwordcheck = $event"
@update:alert="passwordcheckAlert = $event"
@blur="checkPw"
:value="passwordcheck"
/>
<span v-if="passwordcheckError" class="invalid-feedback d-block">{{ passwordcheckError }}</span>
<FormSelect
title="비밀번호 힌트"
name="pwhint"
:is-essential="true"
:is-row="false"
:is-label="true"
:data="pwhintList"
@update:data="pwhint = $event"
/>
<UserFormInput
title="비밀번호 힌트"
name="pwhint"
:is-alert="passwordhintAlert"
@update:data="passwordhint = $event"
@update:alert="passwordhintAlert = $event"
:value="passwordhint"
title="답변"
name="pwhintRes"
:is-essential="true"
:is-alert="pwhintResAlert"
@update:data="pwhintRes = $event"
@update:alert="pwhintResAlert = $event"
:value="pwhintRes"
/>
<div class="d-flex">
@ -74,6 +89,7 @@
:is-essential="true"
:is-row="false"
:is-label="true"
:is-common="true"
:data="colorList"
@update:data="color = $event"
class="w-50"
@ -84,6 +100,7 @@
<UserFormInput
title="생년월일"
name="birth"
:type="'date'"
:is-essential="true"
:is-alert="birthAlert"
@update:data="birth = $event"
@ -98,6 +115,7 @@
:is-essential="true"
:is-row="false"
:is-label="true"
:is-common="true"
:data="mbtiList"
@update:data="mbti = $event"
class="w-50"
@ -109,7 +127,7 @@
name="arr"
:isEssential="true"
:is-alert="arrAlert"
@update:data="arr = $event"
@update:data="handleAddressUpdate"
@update:alert="arrAlert = $event"
:value="arr"
/>
@ -134,69 +152,80 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import $api from '@api';
import UserFormInput from '@c/input/UserFormInput.vue';
import FormSelect from '@/components/input/FormSelect.vue';
import ArrInput from '@/components/input/ArrInput.vue';
import FormSelect from '@c/input/FormSelect.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 profilerr = ref('');
const id = ref('');
const idError = ref('');
const password = ref('');
const passwordcheck = ref('');
const passwordhint = ref('');
const passwordcheckError = ref('');
const pwhint = ref(0);
const pwhintRes = ref('');
const name = ref('');
const birth = ref('');
const arr = ref('');
const arr = ref(''); //
const postcode = ref(''); //
const phone = ref('');
const color = ref('');
const mbti = ref('');
const colorList = ref([]);
const mbtiList = ref([]);
const color = ref(''); // color
const mbti = ref(''); // MBTI
const profilAlert = ref(false);
const idAlert = ref(false);
const idErrorAlert = ref(false);
const passwordAlert = ref(false);
const passwordcheckAlert = ref(false);
const passwordhintAlert = ref(false);
const passwordcheckErrorAlert = ref(false); //
const pwhintResAlert = ref(false);
const nameAlert = ref(false);
const birthAlert = ref(false);
const arrAlert = ref(false);
const phoneAlert = ref(false);
const handleSubmit = async () => {
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() === '';
const toastStore = useToastStore();
if (!profile.value) {
profilerr.value = '프로필 이미지를 선택해주세요.';
return;
}
};
//
const profileValid = (size, type) => {
const maxSize = 5 * 1024 * 1024;
const validTypes = ['image/jpeg', 'image/png'];
//
if (size > maxSize) {
profilerr.value = '5MB 미만의 파일만 업로드할 수 있습니다.';
profilerr.value = fileMsg.FileMaxSizeMsg;
return false;
}
//
if (!validTypes.includes(type)) {
profilerr.value = '지원되지 않는 파일 형식입니다. JPEG 또는 PNG만 가능합니다.';
profilerr.value = fileMsg.FileNotTypeMsg;
return false;
}
profilerr.value = '';
return true;
};
//
const profileUpload = e => {
const file = e.target.files[0];
const profileLabel = document.getElementById('profileLabel');
// ,
if (!profileValid(file.size, file.type)) {
e.target.value = '';
profileLabel.style.backgroundImage = 'url("public/img/avatars/default-Profile.jpg")';
@ -209,8 +238,114 @@
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 () => {
try {
const response = await $api.get('/user/color');
colorList.value = response.data.data.map(item => ({
label: item.CMNCODNAM,
value: item.CMNCODVAL
}));
} catch (error) {
console.error('Error colors:', error);
}
};
const Mbtis = async () => {
try {
const response = await $api.get('/user/mbti');
mbtiList.value = response.data.data.map(item => ({
label: item.CMNCODNAM,
value: item.CMNCODVAL
}));
} catch (error) {
console.error('Error MBTIs:', error);
}
};
onMounted(() => {
Colors();
Mbtis();
});
//
const handleAddressUpdate = (addressData) => {
arr.value = addressData.fullAddress; //
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() === '';
arrAlert.value = arr.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 || arrAlert.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', arr.value); //
formData.append('memberZip', postcode.value); //
formData.append('memberTel', phone.value);
formData.append('memberCol', color.value);
formData.append('memberMbt', mbti.value);
if (profile.value) {
formData.append('profile', profile.value);
}
const response = await $api.post('/user/join', formData, { isFormData : true });
if (response.status === 200) {
toastStore.onToast('등록신청이 완료되었습니다. 관리자 승인 후 이용가능합니다.', 's');
router.push('/login');
}
};
</script>
<style></style>

View File

@ -2,7 +2,7 @@
<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">
<LogoHeader title="LOCALNET" />
<LoginForm @submit="handleSubmit" />
<LoginForm />
</div>
</div>
</template>
@ -11,9 +11,6 @@
import LoginForm from '@c/user/LoginForm.vue';
import LogoHeader from '@c/user/LogoHeader.vue';
const handleSubmit = async ({ id, password }) => {
console.log('Login');
};
</script>
<style></style>

View File

@ -1,7 +1,7 @@
<template>
<div class="d-flex justify-content-center align-items-center">
<div class="container rounded bg-white my-10 py-10" style="max-width: 500px">
<RegisterForm @submit="handleSubmit" />
<RegisterForm/>
</div>
</div>
</template>
@ -9,9 +9,6 @@
<script setup>
import RegisterForm from '@c/user/RegisterForm.vue';
const handleSubmit = async (formData) => {
console.log('Register');
};
</script>
<style></style>