diff --git a/TestMasterUI/src/App.jsx b/TestMasterUI/src/App.jsx index 66b93ec..76ddd99 100644 --- a/TestMasterUI/src/App.jsx +++ b/TestMasterUI/src/App.jsx @@ -32,6 +32,7 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { showTCPServerEdit: false, showQueueEdit: false, showUserEdit: false, + showPasswordChange: false, selectedItem: null, }); @@ -77,6 +78,9 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { case 'variables-show': setDialogState(prev => ({ ...prev, showVariableModal: true })); break; + case 'change-password': + setDialogState(prev => ({ ...prev, showPasswordChange: true })); + break; case 'logout': const success = await handleLogout(); if (success) { @@ -139,6 +143,7 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { showQueueEdit={dialogState.showQueueEdit} showUserEdit={dialogState.showUserEdit} selectedItem={dialogState.selectedItem} + showPasswordChange={dialogState.showPasswordChange} onDialogChange={handleDialogChange} />
diff --git a/TestMasterUI/src/components/account/PasswordChangeDialog.jsx b/TestMasterUI/src/components/account/PasswordChangeDialog.jsx new file mode 100644 index 0000000..0c5377b --- /dev/null +++ b/TestMasterUI/src/components/account/PasswordChangeDialog.jsx @@ -0,0 +1,155 @@ +import React, { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Label } from '@/components/ui/label'; +import { getCSRFToken } from '@/services/token.js'; + +const PasswordChangeDialog = ({ open, onOpenChange }) => { + const [formData, setFormData] = useState({ + oldPassword: '', + newPassword: '', + newPassword2: '' + }); + const [message, setMessage] = useState({ type: '', content: '' }); + const [isLoading, setIsLoading] = useState(false); + + const resetForm = () => { + setFormData({ + oldPassword: '', + newPassword: '', + newPassword2: '' + }); + setMessage({ type: '', content: '' }); + }; + + const handleClose = () => { + resetForm(); + onOpenChange(false); + }; + + const validateForm = () => { + if (!formData.oldPassword) { + setMessage({ type: 'error', content: '현재 비밀번호를 입력해주세요.' }); + return false; + } + if (!formData.newPassword) { + setMessage({ type: 'error', content: '새 비밀번호를 입력해주세요.' }); + return false; + } + if (formData.newPassword !== formData.newPassword2) { + setMessage({ type: 'error', content: '새 비밀번호가 일치하지 않습니다.' }); + return false; + } + return true; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!validateForm()) return; + + setIsLoading(true); + setMessage({ type: '', content: '' }); + + try { + const response = await fetch('/api/account/password', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': getCSRFToken() + }, + body: JSON.stringify(formData) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || '비밀번호 변경에 실패했습니다.'); + } + + setMessage({ type: 'success', content: '비밀번호가 성공적으로 변경되었습니다.' }); + setTimeout(() => { + handleClose(); + }, 2000); + } catch (error) { + setMessage({ type: 'error', content: error.message }); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + 비밀번호 변경 + +
+
+ {message.content && ( + + {message.content} + + )} +
+ + setFormData(prev => ({ ...prev, oldPassword: e.target.value }))} + className="col-span-3" + required + /> +
+
+ + setFormData(prev => ({ ...prev, newPassword: e.target.value }))} + className="col-span-3" + required + /> +
+
+ + setFormData(prev => ({ ...prev, newPassword2: e.target.value }))} + className="col-span-3" + required + /> +
+
+ + + + +
+
+
+ ); +}; + +export default PasswordChangeDialog; diff --git a/TestMasterUI/src/components/account/PasswordEditDialog.jsx b/TestMasterUI/src/components/account/PasswordEditDialog.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/TestMasterUI/src/components/shared/DialogManager.jsx b/TestMasterUI/src/components/shared/DialogManager.jsx index c2282da..fe97139 100644 --- a/TestMasterUI/src/components/shared/DialogManager.jsx +++ b/TestMasterUI/src/components/shared/DialogManager.jsx @@ -7,6 +7,7 @@ import MockRouteEditDialog from '@/components/mock-route/MockRouteEditDialog.jsx import TCPServerEditDialog from '@/components/tcp-servers/TCPServerEditDialog.jsx'; import QueueEditDialog from '@/components/queues/QueueEditDialog.jsx'; import UserEditDialog from '@/components/users/UserEditDialog.jsx'; +import PasswordChangeDialog from '@/components/account/PasswordChangeDialog.jsx'; const DialogManager = ({ showAddApi, @@ -18,6 +19,7 @@ const DialogManager = ({ showQueueEdit, showUserEdit, selectedItem, + showPasswordChange, onDialogChange }) => { @@ -58,6 +60,10 @@ const DialogManager = ({ onOpenChange={(isOpen) => onDialogChange('showUserEdit', isOpen)} item={selectedItem} /> + onDialogChange('showPasswordChange', isOpen)} + /> ; } diff --git a/src/main/java/com/eactive/testmaster/user/controller/AccountApiController.java b/src/main/java/com/eactive/testmaster/user/controller/AccountApiController.java index 6db819d..7f89652 100644 --- a/src/main/java/com/eactive/testmaster/user/controller/AccountApiController.java +++ b/src/main/java/com/eactive/testmaster/user/controller/AccountApiController.java @@ -1,23 +1,70 @@ package com.eactive.testmaster.user.controller; import com.eactive.testmaster.common.util.SecurityUtil; +import com.eactive.testmaster.user.dto.PasswordChangeRequestDTO; import com.eactive.testmaster.user.entity.User; +import com.eactive.testmaster.user.service.UserService; +import java.util.HashMap; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; +import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.ModelMap; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Validator; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController +@RequestMapping("/api/account") public class AccountApiController { - public AccountApiController() { + private final Validator validator; + private final UserService userService; + + public AccountApiController(Validator validator, UserService userService) { + this.validator = validator; + this.userService = userService; } - @GetMapping("/api/account") + @GetMapping @Secured("ROLE_API_TESTER") public ResponseEntity account(ModelMap model) { return ResponseEntity.ok().body(SecurityUtil.getCurrentUser()); } + + @PostMapping("/password") + @Transactional + @Secured("ROLE_API_TESTER") + public ResponseEntity updatePassword(@RequestBody PasswordChangeRequestDTO passwordChangeRequestDTO) { + BindingResult result = new BeanPropertyBindingResult(passwordChangeRequestDTO, "passwordChangeRequest"); + validator.validate(passwordChangeRequestDTO, result); + + if (result.hasErrors()) { + return ResponseEntity.badRequest().body(result.getAllErrors()); + } + + try { + String resultMsg = userService.updateUserPassword( + SecurityUtil.getCurrentUserEsntlId(), + passwordChangeRequestDTO.getOldPassword(), + passwordChangeRequestDTO.getNewPassword(), + passwordChangeRequestDTO.getNewPassword2() + ); + + return ResponseEntity.ok().body(new HashMap() {{ + put("message", resultMsg); + }}); + } catch (Exception e) { + return ResponseEntity.badRequest().body(new HashMap() {{ + put("error", e.getMessage()); + }}); + } + } + }