332 lines
12 KiB
Vue
332 lines
12 KiB
Vue
<template>
|
|
<div class="container-xxl flex-grow-1 container-p-y">
|
|
<div class="card shadow-sm rounded-lg p-6 max-w-2xl mx-auto">
|
|
<h3 class="text-2xl font-semibold mb-3 text-center">마이 페이지</h3>
|
|
<form @submit.prevent="handleSubmit">
|
|
<div class="text-center">
|
|
<label
|
|
for="profilePic"
|
|
class="rounded-circle cursor-pointer"
|
|
id="profileLabel"
|
|
:style="profilePreviewStyle"
|
|
></label>
|
|
<input type="file" id="profilePic" class="d-none object-fit-cover" @change="profileUpload" />
|
|
<span v-if="profilerr" class="invalid-feedback d-block">{{ profilerr }}</span>
|
|
</div>
|
|
|
|
<div class="col-xl-12">
|
|
<div class="d-flex">
|
|
<div class="w-50 me-2">
|
|
<UserFormInput
|
|
title="입사일"
|
|
name="entryDate"
|
|
type="date"
|
|
:value="form.entryDate"
|
|
@update:data="form.entryDate = $event"
|
|
/>
|
|
</div>
|
|
|
|
<div class="d-flex flex-column w-50">
|
|
<FormSelect
|
|
title="컬러"
|
|
name="color"
|
|
:is-row="false"
|
|
:is-label="true"
|
|
:is-common="true"
|
|
:is-color="true"
|
|
:data="colorList"
|
|
:value="form.color"
|
|
@update:data="handleColorUpdate"
|
|
/>
|
|
<span v-if="colorDuplicated" class="text-danger invalid-feedback mt-1 d-block">
|
|
이미 사용 중인 컬러입니다.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex">
|
|
<UserFormInput title="생년월일" name="birth" type="date"
|
|
:value="form.birth" @update:data="form.birth = $event" class="me-2 w-50" />
|
|
<FormSelect title="MBTI" name="mbti" :is-row="false" :is-label="true"
|
|
:is-common="true" :is-mbti="true" :data="mbtiList"
|
|
:value="form.mbti" @update:data="form.mbti = $event" class="w-50" />
|
|
</div>
|
|
|
|
<ArrInput title="주소" name="address" v-model="form.address" :disabled="true" />
|
|
|
|
<UserFormInput title="전화번호" name="phone" :value="form.phone"
|
|
@update:data="form.phone = $event" @blur="checkPhoneDuplicateAndFormat"
|
|
: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>
|
|
|
|
<!-- 기존 비밀번호 입력 -->
|
|
<UserFormInput title="기존 비밀번호" name="currentPw" type="password"
|
|
:value="password.current" @update:data="password.current = $event"
|
|
@blur="checkCurrentPassword" @keypress="noSpace" />
|
|
<span v-if="passwordError" class="text-danger invalid-feedback mt-1 d-block">
|
|
비밀번호가 일치하지 않습니다.
|
|
</span>
|
|
|
|
<!-- 비밀번호 재설정 -->
|
|
<div v-if="showResetPw">
|
|
<UserFormInput title="새 비밀번호" name="newPw" type="password"
|
|
:value="password.new" @update:data="password.new = $event" @keypress="noSpace" />
|
|
<span v-if="password.new && password.new.length < 4"
|
|
class="text-danger invalid-feedback mt-1 d-block">
|
|
새 비밀번호는 최소 4자리 이상이어야 합니다.
|
|
</span>
|
|
<span v-if="password.new === password.current"
|
|
class="text-danger invalid-feedback mt-1 d-block">
|
|
기존 비밀번호와 다르게 설정해주세요.
|
|
</span>
|
|
|
|
<UserFormInput title="비밀번호 확인" name="confirmPw" type="password"
|
|
:value="password.confirm" @update:data="password.confirm = $event" @keypress="noSpace" />
|
|
<span v-if="password.confirm && password.confirm !== password.new"
|
|
class="text-danger invalid-feedback mt-1 d-block">
|
|
새 비밀번호와 일치하지 않습니다.
|
|
</span>
|
|
|
|
<div class="d-flex justify-content-end mt-2">
|
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
|
:disabled="!canResetPassword"
|
|
@click="handlePasswordReset">
|
|
비밀번호 변경
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex mt-5">
|
|
<button type="submit" class="btn btn-primary w-100"
|
|
:disabled="!isChanged || phoneDuplicated || phoneFormatError || colorDuplicated">
|
|
정보 수정
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, watch} from 'vue';
|
|
import $api from '@api';
|
|
import UserFormInput from '@c/input/UserFormInput.vue';
|
|
import FormSelect from '@c/input/FormSelect.vue';
|
|
import ArrInput from '@c/input/ArrInput.vue';
|
|
import { useToastStore } from '@s/toastStore';
|
|
|
|
const toastStore = useToastStore();
|
|
|
|
const noSpace = (e) => {
|
|
if (e.key === ' ') e.preventDefault();
|
|
};
|
|
|
|
const form = ref({
|
|
entryDate: '', birth: '', phone: '', color: '', mbti: '',
|
|
address: { address: '', detailAddress: '', postcode: '' },
|
|
id: ''
|
|
});
|
|
const originalData = ref({});
|
|
const profile = ref('');
|
|
const uploadedFile = ref(null);
|
|
const profileChanged = ref(false);
|
|
const profilerr = ref('');
|
|
const currentBlobUrl = ref('');
|
|
const colorDuplicated = ref(false);
|
|
const phoneDuplicated = ref(false);
|
|
const mbtiList = ref([]);
|
|
const colorList = ref([]);
|
|
|
|
const password = ref({ current: '', new: '', confirm: '' });
|
|
const passwordError = ref(false);
|
|
const phoneFormatError = ref(false);
|
|
const showResetPw = ref(false);
|
|
|
|
const canResetPassword = computed(() => {
|
|
return (
|
|
password.value.new.length >= 4 &&
|
|
password.value.new !== password.value.current &&
|
|
password.value.new === password.value.confirm
|
|
);
|
|
});
|
|
|
|
watch(
|
|
() => form.value.address.detailAddress,
|
|
(newVal) => {
|
|
if (newVal !== newVal.trim()) {
|
|
form.value.address.detailAddress = newVal.trim();
|
|
}
|
|
}
|
|
);
|
|
|
|
const isChanged = computed(() => {
|
|
const f = form.value;
|
|
const o = originalData.value;
|
|
return (
|
|
f.entryDate !== o.entryDate || f.birth !== o.birth || f.phone !== o.phone ||
|
|
f.color !== o.color || f.mbti !== o.mbti || profileChanged.value ||
|
|
f.address.address !== o.address.address ||
|
|
f.address.detailAddress !== o.address.detailAddress ||
|
|
f.address.postcode !== o.address.postcode
|
|
);
|
|
});
|
|
|
|
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, '');
|
|
const defaultProfile = "/img/icons/icon.png";
|
|
const getProfileImageUrl = (profilePath) =>
|
|
profilePath && profilePath.trim() ? `${baseUrl}upload/img/profile/${profilePath}` : defaultProfile;
|
|
const profilePreviewStyle = computed(() => ({
|
|
width: '100px',
|
|
height: '100px',
|
|
backgroundImage: `url(${profile.value})`,
|
|
backgroundRepeat: 'no-repeat',
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center'
|
|
}));
|
|
|
|
const profileUpload = (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
if (file.size > 5 * 1024 * 1024 || !['image/jpeg', 'image/png'].includes(file.type)) {
|
|
profilerr.value = '5MB 이하의 JPG/PNG 파일만 업로드 가능합니다.';
|
|
return;
|
|
}
|
|
profilerr.value = '';
|
|
if (currentBlobUrl.value) URL.revokeObjectURL(currentBlobUrl.value);
|
|
uploadedFile.value = file;
|
|
const newBlobUrl = URL.createObjectURL(file);
|
|
profile.value = newBlobUrl;
|
|
currentBlobUrl.value = newBlobUrl;
|
|
profileChanged.value = true;
|
|
};
|
|
|
|
const onlyNumber = (e) => {
|
|
if (!/[0-9]/.test(e.key)) e.preventDefault();
|
|
};
|
|
|
|
const checkPhoneDuplicateAndFormat = async () => {
|
|
const currentPhone = form.value.phone.trim();
|
|
|
|
// 형식 검사 (010으로 시작 + 숫자 8자리)
|
|
const phoneRegex = /^010\d{8}$/;
|
|
const isFormatValid = phoneRegex.test(currentPhone);
|
|
|
|
phoneFormatError.value = !isFormatValid;
|
|
|
|
// 중복 체크는 형식이 맞을 때만 수행
|
|
if (isFormatValid) {
|
|
const response = await $api.get('/user/checkPhone', {
|
|
params: { memberTel: currentPhone },
|
|
});
|
|
|
|
// true면 사용 가능하다는 의미니까 → 중복 아님
|
|
// false면 중복된 번호라는 의미니까 → 중복됨
|
|
phoneDuplicated.value = currentPhone !== originalData.value.phone && !response.data.data;
|
|
} else {
|
|
// 형식이 맞지 않으면 중복 여부는 무시 (false로 초기화)
|
|
phoneDuplicated.value = false;
|
|
}
|
|
};
|
|
|
|
const handleColorUpdate = async (colorVal) => {
|
|
form.value.color = colorVal;
|
|
colorDuplicated.value = colorVal !== originalData.value.color &&
|
|
(await $api.get('/user/checkColor', { params: { memberCol: colorVal } })).data.data;
|
|
};
|
|
|
|
const checkCurrentPassword = async () => {
|
|
if (!password.value.current) return;
|
|
const res = await $api.post('/user/checkPassword', {
|
|
id: form.value.id,
|
|
password: password.value.current
|
|
});
|
|
passwordError.value = res.data.data;
|
|
showResetPw.value = !res.data.data;
|
|
};
|
|
const handlePasswordReset = async () => {
|
|
const res = await $api.patch('/user/pwNew', {
|
|
id: form.value.id,
|
|
password: password.value.new
|
|
});
|
|
if (res.data.data) {
|
|
toastStore.onToast('비밀번호가 변경되었습니다.', 's');
|
|
password.value = { current: '', new: '', confirm: '' };
|
|
showResetPw.value = false;
|
|
passwordError.value = false;
|
|
} else {
|
|
toastStore.onToast('비밀번호 변경 실패', 'e');
|
|
}
|
|
};
|
|
|
|
const formatDate = (isoDate) => isoDate?.split('T')[0] || '';
|
|
|
|
const loadInitialData = async () => {
|
|
const user = (await $api.get('/user/userInfo')).data.data;
|
|
const serverColors = (await $api.get('/user/color', { params: { type: 'YON' } })).data.data.map(c => ({
|
|
value: c.CMNCODVAL, label: c.CMNCODNAM
|
|
}));
|
|
const matchedColor = serverColors.find(c => c.label === user.usercolor);
|
|
const colorCode = matchedColor ? matchedColor.value : user.color;
|
|
colorList.value = serverColors.some(c => c.value === colorCode)
|
|
? serverColors
|
|
: [{ value: colorCode, label: user.usercolor }, ...serverColors];
|
|
|
|
const initData = {
|
|
id: user.loginId,
|
|
entryDate: formatDate(user.isCdt),
|
|
birth: formatDate(user.birth),
|
|
phone: user.phone || '',
|
|
color: colorCode,
|
|
mbti: user.mbit || '',
|
|
address: {
|
|
address: user.address || '',
|
|
detailAddress: user.addressDetail || '',
|
|
postcode: user.zipcode || ''
|
|
}
|
|
};
|
|
form.value = { ...initData };
|
|
originalData.value = { ...initData };
|
|
profile.value = getProfileImageUrl(user.profile);
|
|
profileChanged.value = false;
|
|
|
|
const mbtiRes = await $api.get('/user/mbti');
|
|
mbtiList.value = mbtiRes.data.data.map(m => ({ value: m.CMNCODVAL, label: m.CMNCODNAM }));
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
const formData = new FormData();
|
|
Object.entries(form.value).forEach(([k, v]) => {
|
|
if (typeof v === 'object') {
|
|
formData.append('address', v.address);
|
|
formData.append('detailAddress', v.detailAddress);
|
|
formData.append('postcode', v.postcode);
|
|
} else {
|
|
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' });
|
|
}
|
|
|
|
await $api.patch('/user/updateInfo', formData, { isFormData: true });
|
|
originalData.value = { ...form.value };
|
|
profileChanged.value = false;
|
|
location.reload();
|
|
toastStore.onToast('정보가 수정되었습니다.', 's');
|
|
};
|
|
|
|
onMounted(() => loadInitialData());
|
|
</script>
|
|
|
|
<style scoped>
|
|
</style>
|