프로필 수정 업데이트
This commit is contained in:
@@ -164,11 +164,10 @@ exports.getCurrentUser = async (req, res) => {
|
|||||||
// 프로필 업데이트
|
// 프로필 업데이트
|
||||||
exports.updateProfile = async (req, res) => {
|
exports.updateProfile = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.user.id;
|
const { id, name, email, password, currentPassword } = req.body;
|
||||||
const { name, email, password, currentPassword } = req.body;
|
|
||||||
|
|
||||||
// 사용자 조회
|
// 사용자 조회
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(id);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
|
return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
|
||||||
@@ -183,34 +182,39 @@ exports.updateProfile = async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이메일이 변경되는 경우
|
// 이메일이 변경된 경우 중복 확인
|
||||||
if (email && email !== user.email) {
|
if (email && email !== user.getDataValue('email')) {
|
||||||
// Users 테이블에서 이메일 중복 확인
|
// 이메일 유효성 검사 (암호화 전)
|
||||||
const existingUser = await User.findOne({ where: { email } });
|
if (!/^\S+@\S+\.\S+$/.test(email)) {
|
||||||
|
return res.status(400).json({ message: '유효한 이메일 형식이 아닙니다.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이메일 중복 확인 (암호화된 값으로 비교할 수 없으므로 복호화하여 비교)
|
||||||
|
const allUsers = await User.findAll();
|
||||||
|
const existingUser = allUsers.find(u => u.email === email && u.id !== id);
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return res.status(400).json({ message: '이미 사용 중인 이메일입니다.' });
|
return res.status(400).json({ message: '이미 사용 중인 이메일입니다.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Members 테이블에서 이메일 중복 확인
|
|
||||||
const existingMember = await Member.findOne({ where: { email } });
|
|
||||||
if (existingMember) {
|
|
||||||
return res.status(400).json({ message: '이미 클럽 회원이 사용 중인 이메일입니다.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 해당 사용자의 Members 테이블 레코드도 함께 업데이트
|
// 해당 사용자의 Members 테이블 레코드도 함께 업데이트
|
||||||
await Member.update(
|
await Member.update(
|
||||||
{ email },
|
{ email, name },
|
||||||
{ where: { userId } }
|
{ where: { userId: id } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 프로필 업데이트
|
// 프로필 업데이트
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: name || user.name,
|
name: name || user.name
|
||||||
email: email || user.email
|
|
||||||
// username은 변경하지 않음 - 로그인 식별자로 사용되기 때문
|
// username은 변경하지 않음 - 로그인 식별자로 사용되기 때문
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 이메일은 별도로 처리 (암호화되어 있기 때문에)
|
||||||
|
if (email) {
|
||||||
|
updateData.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
// 비밀번호가 제공된 경우에만 업데이트
|
// 비밀번호가 제공된 경우에만 업데이트
|
||||||
if (password && currentPassword) {
|
if (password && currentPassword) {
|
||||||
updateData.password = password;
|
updateData.password = password;
|
||||||
@@ -219,7 +223,7 @@ exports.updateProfile = async (req, res) => {
|
|||||||
await user.update(updateData);
|
await user.update(updateData);
|
||||||
|
|
||||||
// 업데이트된 사용자 정보 반환 (비밀번호 제외)
|
// 업데이트된 사용자 정보 반환 (비밀번호 제외)
|
||||||
const updatedUser = await User.findByPk(userId, {
|
const updatedUser = await User.findByPk(id, {
|
||||||
attributes: { exclude: ['password'] }
|
attributes: { exclude: ['password'] }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ const User = sequelize.define('User', {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true,
|
unique: true,
|
||||||
validate: {
|
|
||||||
isEmail: true
|
|
||||||
},
|
|
||||||
comment: '이메일 (암호화됨)',
|
comment: '이메일 (암호화됨)',
|
||||||
get() {
|
get() {
|
||||||
const rawValue = this.getDataValue('email');
|
const rawValue = this.getDataValue('email');
|
||||||
|
|||||||
@@ -1,59 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="profile-container">
|
<div class="profile-container">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
<h2>내 프로필</h2>
|
<h2>내 프로필</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="profile-content">
|
<div class="profile-content">
|
||||||
<div class="profile-header">
|
<div class="profile-header">
|
||||||
<div class="profile-avatar">
|
<div class="profile-avatar">
|
||||||
<Avatar :image="user.avatar || null" :label="getInitials(user.name)" size="xlarge" shape="circle" />
|
<Avatar :image="user.avatar || null" :label="getInitials(user.name)" size="xlarge" shape="circle" class="shadow-2" />
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-info">
|
<div class="profile-info">
|
||||||
<h3>{{ user.name }}</h3>
|
<h3>{{ user.name }}</h3>
|
||||||
<p>{{ user.email }}</p>
|
<div class="info-item">
|
||||||
<p><Badge :value="user.role === 'admin' ? '관리자' : '일반 사용자'" :severity="user.role === 'admin' ? 'success' : 'info'" /></p>
|
<span>{{ user.username }} <small>(로그인 아이디)</small></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span>{{ user.email }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<Badge :value="user.role === 'admin' ? '관리자' : '일반 사용자'" :severity="user.role === 'admin' ? 'success' : 'info'" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider />
|
<Divider>
|
||||||
|
<span class="text-sm text-500">프로필 정보 수정</span>
|
||||||
|
</Divider>
|
||||||
|
|
||||||
<div class="profile-form">
|
<div class="profile-form">
|
||||||
<form @submit.prevent="updateProfile">
|
<form @submit.prevent="updateProfile">
|
||||||
<div class="p-fluid">
|
<div class="p-fluid formgrid grid">
|
||||||
<div class="field">
|
<div class="field col-12 md:col-6">
|
||||||
<label for="name">이름</label>
|
<label for="name" class="font-medium mb-2 block">이름</label>
|
||||||
<InputText id="name" v-model="form.name" :class="{'p-invalid': v$.name.$invalid && submitted}" />
|
<InputText id="name" v-model="form.name" :class="{'p-invalid': v$.name.$invalid && submitted}" class="w-full" />
|
||||||
<small v-if="v$.name.$invalid && submitted" class="p-error">{{ v$.name.$errors[0].$message }}</small>
|
<small v-if="v$.name.$invalid && submitted" class="p-error block mt-1">{{ v$.name.$errors[0].$message }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field col-12 md:col-6">
|
||||||
<label for="email">이메일</label>
|
<label for="email" class="font-medium mb-2 block">이메일</label>
|
||||||
<InputText id="email" v-model="form.email" :class="{'p-invalid': v$.email.$invalid && submitted}" />
|
<InputText id="email" v-model="form.email" :class="{'p-invalid': v$.email.$invalid && submitted}" class="w-full" />
|
||||||
<small v-if="v$.email.$invalid && submitted" class="p-error">{{ v$.email.$errors[0].$message }}</small>
|
<small v-if="v$.email.$invalid && submitted" class="p-error block mt-1">{{ v$.email.$errors[0].$message }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider />
|
<div class="col-12">
|
||||||
|
<Divider>
|
||||||
<div class="field">
|
<span class="text-sm text-500">비밀번호 변경</span>
|
||||||
<label for="currentPassword">현재 비밀번호 (변경 시에만 입력)</label>
|
</Divider>
|
||||||
<Password id="currentPassword" v-model="form.currentPassword" :feedback="false" toggleMask />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field col-12 md:col-4">
|
||||||
<label for="newPassword">새 비밀번호 (변경 시에만 입력)</label>
|
<label for="currentPassword" class="font-medium mb-2 block">현재 비밀번호</label>
|
||||||
<Password id="newPassword" v-model="form.newPassword" :class="{'p-invalid': v$.newPassword.$invalid && passwordChangeAttempted}" toggleMask />
|
<Password id="currentPassword" v-model="form.currentPassword" :feedback="false" toggleMask inputClass="w-full" />
|
||||||
<small v-if="v$.newPassword.$invalid && passwordChangeAttempted" class="p-error">{{ v$.newPassword.$errors[0].$message }}</small>
|
<small class="text-500 block mt-1">변경 시에만 입력</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field col-12 md:col-4">
|
||||||
<label for="confirmPassword">비밀번호 확인</label>
|
<label for="newPassword" class="font-medium mb-2 block">새 비밀번호</label>
|
||||||
<Password id="confirmPassword" v-model="form.confirmPassword" :class="{'p-invalid': v$.confirmPassword.$invalid && passwordChangeAttempted}" :feedback="false" toggleMask />
|
<Password id="newPassword" v-model="form.newPassword" :class="{'p-invalid': v$.newPassword.$invalid && passwordChangeAttempted}" toggleMask inputClass="w-full" />
|
||||||
<small v-if="v$.confirmPassword.$invalid && passwordChangeAttempted" class="p-error">{{ v$.confirmPassword.$errors[0].$message }}</small>
|
<small v-if="v$.newPassword.$invalid && passwordChangeAttempted" class="p-error block mt-1">{{ v$.newPassword.$errors[0].$message }}</small>
|
||||||
|
<small v-else class="text-500 block mt-1">변경 시에만 입력</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field col-12 md:col-4">
|
||||||
|
<label for="confirmPassword" class="font-medium mb-2 block">비밀번호 확인</label>
|
||||||
|
<Password id="confirmPassword" v-model="form.confirmPassword" :class="{'p-invalid': v$.confirmPassword.$invalid && passwordChangeAttempted}" :feedback="false" toggleMask inputClass="w-full" />
|
||||||
|
<small v-if="v$.confirmPassword.$invalid && passwordChangeAttempted" class="p-error block mt-1">{{ v$.confirmPassword.$errors[0].$message }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-container">
|
<div class="button-container">
|
||||||
<Button type="submit" label="프로필 업데이트" :loading="loading" />
|
<Button type="submit" label="프로필 업데이트" icon="pi pi-save" :loading="loading" class="p-button-raised" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,10 +86,10 @@ import { ref, reactive, computed, onMounted } from 'vue';
|
|||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { required, email, minLength, sameAs, helpers } from '@vuelidate/validators';
|
import { required, email, minLength, sameAs, helpers } from '@vuelidate/validators';
|
||||||
import axios from 'axios';
|
import authService from '@/services/authService';
|
||||||
|
import apiClient from '@/services/api';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const API_URL = 'http://localhost:3030/api';
|
|
||||||
|
|
||||||
// 상태 변수
|
// 상태 변수
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -83,6 +100,7 @@ const passwordChangeAttempted = ref(false);
|
|||||||
const user = reactive({
|
const user = reactive({
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
role: '',
|
role: '',
|
||||||
avatar: null
|
avatar: null
|
||||||
@@ -90,6 +108,7 @@ const user = reactive({
|
|||||||
|
|
||||||
// 폼 데이터
|
// 폼 데이터
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
currentPassword: '',
|
currentPassword: '',
|
||||||
@@ -122,15 +141,13 @@ onMounted(async () => {
|
|||||||
// 사용자 프로필 가져오기
|
// 사용자 프로필 가져오기
|
||||||
const fetchUserProfile = async () => {
|
const fetchUserProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const response = await authService.getCurrentUser();
|
||||||
const response = await axios.get(`${API_URL}/auth/me`, {
|
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 사용자 정보 설정
|
// 사용자 정보 설정
|
||||||
Object.assign(user, response.data);
|
Object.assign(user, response.data);
|
||||||
|
|
||||||
// 폼 데이터 초기화
|
// 폼 데이터 초기화
|
||||||
|
form.username = user.username;
|
||||||
form.name = user.name;
|
form.name = user.name;
|
||||||
form.email = user.email;
|
form.email = user.email;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -157,8 +174,8 @@ const updateProfile = async () => {
|
|||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
const profileData = {
|
const profileData = {
|
||||||
|
id: user.id,
|
||||||
name: form.name,
|
name: form.name,
|
||||||
email: form.email
|
email: form.email
|
||||||
};
|
};
|
||||||
@@ -176,9 +193,7 @@ const updateProfile = async () => {
|
|||||||
profileData.password = form.newPassword;
|
profileData.password = form.newPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.put(`${API_URL}/auth/profile`, profileData, {
|
await apiClient.put('/api/auth/profile', profileData);
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 사용자 정보 업데이트
|
// 사용자 정보 업데이트
|
||||||
Object.assign(user, {
|
Object.assign(user, {
|
||||||
@@ -204,39 +219,60 @@ const updateProfile = async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.profile-container {
|
.profile-container {
|
||||||
max-width: 800px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
padding: 2rem;
|
border-radius: 12px;
|
||||||
border-radius: 10px;
|
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||||
box-shadow: 0 2px 1px -1px rgba(0,0,0,0.2), 0 1px 1px 0 rgba(0,0,0,0.14), 0 1px 3px 0 rgba(0,0,0,0.12);
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: var(--primary-color, #3B82F6);
|
||||||
|
color: white;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-content {
|
.profile-content {
|
||||||
margin-top: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-header {
|
.profile-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2rem;
|
gap: 2.5rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-info {
|
.profile-info {
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--text-color-secondary, #4b5563);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.info-item {
|
||||||
margin: 0;
|
margin-bottom: 0.75rem;
|
||||||
margin-bottom: 0.5rem;
|
color: var(--text-color-secondary, #6b7280);
|
||||||
color: #666;
|
|
||||||
|
small {
|
||||||
|
color: var(--text-color-secondary, #9ca3af);
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +288,8 @@ const updateProfile = async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2.5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--surface-border, #e5e7eb);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user