feat: [users] 사용자 등록 시 비밀번호 자동 생성 및 이메일 발송
- 사용자 등록 시 비밀번호 입력 필드 제거 - 임의 비밀번호 자동 생성 후 이메일 발송 - 사용자 수정 페이지에 비밀번호 초기화 버튼 추가 - 사용자 모달에 비밀번호 초기화 버튼 추가 - 사용자 모달 프로필 이미지 없을 때 이름 첫글자 표시 (한글 지원) - UserPasswordMail 클래스 및 이메일 템플릿 추가
This commit is contained in:
@@ -228,6 +228,41 @@ public function modal(Request $request, int $id): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 초기화 (임의 비밀번호 생성 + 메일 발송)
|
||||
*/
|
||||
public function resetPassword(Request $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 비밀번호 초기화 불가
|
||||
if (! $this->userService->canAccessUser($id)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사용자를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$result = $this->userService->resetPassword($id);
|
||||
|
||||
if (! $result) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사용자를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '비밀번호가 초기화되었습니다. 새 비밀번호가 사용자 이메일로 발송되었습니다.',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '비밀번호 초기화에 실패했습니다: '.$e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 영구 삭제 (슈퍼관리자 전용)
|
||||
*/
|
||||
|
||||
@@ -40,7 +40,7 @@ public function rules(): array
|
||||
'name' => 'required|string|max:100',
|
||||
'email' => 'required|email|max:255|unique:users,email',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
// password는 시스템이 자동 생성하므로 입력 받지 않음
|
||||
'role' => 'nullable|string|max:50',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'is_super_admin' => 'nullable|boolean',
|
||||
|
||||
58
app/Mail/UserPasswordMail.php
Normal file
58
app/Mail/UserPasswordMail.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserPasswordMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public User $user,
|
||||
public string $password,
|
||||
public bool $isNewUser = true
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$subject = $this->isNewUser
|
||||
? '[SAM] 계정이 생성되었습니다'
|
||||
: '[SAM] 비밀번호가 초기화되었습니다';
|
||||
|
||||
return new Envelope(
|
||||
subject: $subject,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.user-password',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Mail\UserPasswordMail;
|
||||
use App\Models\DepartmentUser;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRole;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class UserService
|
||||
{
|
||||
@@ -80,16 +82,15 @@ public function getUserById(int $id): ?User
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 생성
|
||||
* 사용자 생성 (관리자용: 임의 비밀번호 생성 + 메일 발송)
|
||||
*/
|
||||
public function createUser(array $data): User
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
// 비밀번호 해싱
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
}
|
||||
// 임의 비밀번호 생성 (8자리 영문+숫자)
|
||||
$plainPassword = $this->generateRandomPassword();
|
||||
$data['password'] = Hash::make($plainPassword);
|
||||
|
||||
// is_active 처리
|
||||
$data['is_active'] = isset($data['is_active']) && $data['is_active'] == '1';
|
||||
@@ -116,9 +117,56 @@ public function createUser(array $data): User
|
||||
$this->syncDepartments($user, $tenantId, $departmentIds);
|
||||
}
|
||||
|
||||
// 비밀번호 안내 메일 발송
|
||||
$this->sendPasswordMail($user, $plainPassword, true);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 초기화 (관리자용: 임의 비밀번호 생성 + 메일 발송)
|
||||
*/
|
||||
public function resetPassword(int $id): bool
|
||||
{
|
||||
$user = $this->getUserById($id);
|
||||
if (! $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 임의 비밀번호 생성
|
||||
$plainPassword = $this->generateRandomPassword();
|
||||
|
||||
// 비밀번호 업데이트
|
||||
$user->password = Hash::make($plainPassword);
|
||||
$user->updated_by = auth()->id();
|
||||
$user->save();
|
||||
|
||||
// 비밀번호 초기화 안내 메일 발송
|
||||
$this->sendPasswordMail($user, $plainPassword, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 임의 비밀번호 생성 (8자리 영문+숫자 조합)
|
||||
*/
|
||||
private function generateRandomPassword(int $length = 8): string
|
||||
{
|
||||
// 영문 대소문자 + 숫자 조합으로 가독성 좋은 비밀번호 생성
|
||||
// 혼동되는 문자 제외: 0, O, l, 1, I
|
||||
$chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
||||
|
||||
return substr(str_shuffle(str_repeat($chars, 3)), 0, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 안내 메일 발송
|
||||
*/
|
||||
private function sendPasswordMail(User $user, string $password, bool $isNewUser): void
|
||||
{
|
||||
Mail::to($user->email)->send(new UserPasswordMail($user, $password, $isNewUser));
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 수정
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user