feat: 사용자 관리 시스템 백엔드 API 호환성 대폭 개선
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled

- UserRemoteDataSource: API v0.2.1 스펙 완전 대응
  • 응답 형식 통일 (success: true 구조)
  • 페이지네이션 처리 개선
  • 에러 핸들링 강화
  • 불필요한 파라미터 제거 (includeInactive 등)

- UserDto 모델 현대화:
  • 서버 응답 구조와 100% 일치
  • 도메인 모델 변환 메서드 추가
  • Freezed 불변성 패턴 완성

- User 도메인 모델 신규 구현:
  • Clean Architecture 원칙 준수
  • UserRole enum 타입 안전성 강화
  • 비즈니스 로직 캡슐화

- 사용자 관련 UseCase 리팩토링:
  • Repository 패턴 완전 적용
  • Either<Failure, Success> 에러 처리
  • 의존성 주입 최적화

- UI 컨트롤러 및 화면 개선:
  • API 응답 변경사항 반영
  • 사용자 권한 표시 정확성 향상
  • 폼 검증 로직 강화

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-08-15 23:31:51 +09:00
parent c1063f5670
commit 93bceb8a6c
20 changed files with 2006 additions and 2017 deletions

View File

@@ -5,7 +5,7 @@ import 'package:superport/utils/constants.dart';
import 'package:superport/utils/validators.dart';
import 'package:flutter/services.dart';
import 'package:superport/screens/user/controllers/user_form_controller.dart';
import 'package:superport/screens/common/widgets/company_branch_dropdown.dart';
import 'package:superport/models/user_model.dart';
// 사용자 등록/수정 화면 (UI만 담당, 상태/로직 분리)
class UserFormScreen extends StatefulWidget {
@@ -51,9 +51,9 @@ class _UserFormScreenState extends State<UserFormScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 이름
// 이름 (*필수)
_buildTextField(
label: '이름',
label: '이름 *',
initialValue: controller.name,
hintText: '사용자 이름을 입력하세요',
validator: (value) => validateRequired(value, '이름'),
@@ -63,9 +63,9 @@ class _UserFormScreenState extends State<UserFormScreen> {
// 사용자명 (신규 등록 시만)
if (!controller.isEditMode) ...[
_buildTextField(
label: '사용자명',
label: '사용자명 *',
initialValue: controller.username,
hintText: '로그인에 사용할 사용자명',
hintText: '로그인에 사용할 사용자명 (3자 이상)',
validator: (value) {
if (value == null || value.isEmpty) {
return '사용자명을 입력해주세요';
@@ -106,11 +106,11 @@ class _UserFormScreenState extends State<UserFormScreen> {
: null,
),
// 비밀번호
// 비밀번호 (*필수)
_buildPasswordField(
label: '비밀번호',
label: '비밀번호 *',
controller: _passwordController,
hintText: '비밀번호를 입력하세요',
hintText: '비밀번호를 입력하세요 (6자 이상)',
obscureText: !_showPassword,
onToggleVisibility: () {
setState(() {
@@ -197,50 +197,27 @@ class _UserFormScreenState extends State<UserFormScreen> {
),
],
// 직급
_buildTextField(
label: '직급',
initialValue: controller.position,
hintText: '직급을 입력하세요',
onSaved: (value) => controller.position = value ?? '',
),
// 소속 회사/지점
CompanyBranchDropdown(
companies: controller.companies,
selectedCompanyId: controller.companyId,
selectedBranchId: controller.branchId,
branches: controller.branches,
onCompanyChanged: (value) {
controller.companyId = value;
controller.branchId = null;
if (value != null) {
controller.loadBranches(value);
} else {
controller.branches = [];
}
},
onBranchChanged: (value) {
controller.branchId = value;
},
),
// 이메일
// 이메일 (*필수)
_buildTextField(
label: '이메일',
label: '이메일 *',
initialValue: controller.email,
hintText: '이메일을 입력하세요',
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) return null;
if (value == null || value.isEmpty) {
return '이메일을 입력해주세요';
}
return validateEmail(value);
},
onSaved: (value) => controller.email = value ?? '',
onSaved: (value) => controller.email = value!,
),
// 전화번호
_buildPhoneFieldsSection(controller),
// 권한
_buildRoleRadio(controller),
// 전화번호 (선택)
_buildPhoneNumberSection(controller),
// 권한 (*필수)
_buildRoleDropdown(controller),
const SizedBox(height: 24),
// 오류 메시지 표시
if (controller.error != null)
@@ -378,93 +355,136 @@ class _UserFormScreenState extends State<UserFormScreen> {
);
}
// 전화번호 입력 필드 섹션 위젯 (UserPhoneField 기반)
Widget _buildPhoneFieldsSection(UserFormController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('전화번호', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
...controller.phoneFields.asMap().entries.map((entry) {
final i = entry.key;
final phoneField = entry.value;
return Row(
children: [
// 종류 드롭다운
DropdownButton<String>(
value: phoneField.type,
items: controller.phoneTypes
.map((type) => DropdownMenuItem(value: type, child: Text(type)))
.toList(),
onChanged: (value) {
phoneField.type = value!;
},
),
const SizedBox(width: 8),
// 번호 입력
Expanded(
child: TextFormField(
controller: phoneField.controller,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(hintText: '전화번호'),
onSaved: (value) {}, // 값은 controller에서 직접 추출
),
),
IconButton(
icon: const Icon(Icons.remove_circle, color: Colors.red),
onPressed: controller.phoneFields.length > 1
? () => controller.removePhoneField(i)
: null,
),
],
);
}),
// 추가 버튼
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
onPressed: () => controller.addPhoneField(),
icon: const Icon(Icons.add),
label: const Text('전화번호 추가'),
),
),
],
);
}
// 권한(관리등급) 라디오 위젯
Widget _buildRoleRadio(UserFormController controller) {
// 전화번호 입력 섹션 (드롭다운 + 텍스트 필드)
Widget _buildPhoneNumberSection(UserFormController controller) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('권한', style: TextStyle(fontWeight: FontWeight.bold)),
const Text('전화번호', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: RadioListTile<String>(
title: const Text('관리자'),
value: UserRoles.admin,
groupValue: controller.role,
// 접두사 드롭다운 (010, 02, 031 등)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4),
),
child: DropdownButton<String>(
value: controller.phonePrefix,
items: controller.phonePrefixes.map((prefix) {
return DropdownMenuItem(
value: prefix,
child: Text(prefix),
);
}).toList(),
onChanged: (value) {
controller.role = value!;
if (value != null) {
controller.updatePhonePrefix(value);
}
},
underline: Container(), // 밑줄 제거
),
),
const SizedBox(width: 8),
const Text('-', style: TextStyle(fontSize: 16)),
const SizedBox(width: 8),
// 전화번호 입력 (7-8자리)
Expanded(
child: RadioListTile<String>(
title: const Text('일반 사용자'),
value: UserRoles.member,
groupValue: controller.role,
child: TextFormField(
initialValue: controller.phoneNumber,
decoration: const InputDecoration(
hintText: '1234567 또는 12345678',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(8),
],
validator: (value) {
if (value != null && value.isNotEmpty) {
if (value.length < 7 || value.length > 8) {
return '전화번호는 7-8자리 숫자를 입력해주세요';
}
}
return null;
},
onChanged: (value) {
controller.role = value!;
controller.updatePhoneNumber(value);
},
onSaved: (value) {
if (value != null) {
controller.updatePhoneNumber(value);
}
},
),
),
],
),
if (controller.combinedPhoneNumber.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'전화번호: ${controller.combinedPhoneNumber}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
),
],
),
);
}
// 권한 드롭다운 (새 UserRole 시스템)
Widget _buildRoleDropdown(UserFormController controller) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('권한 *', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
DropdownButtonFormField<UserRole>(
value: controller.role,
decoration: const InputDecoration(
hintText: '권한을 선택하세요',
border: OutlineInputBorder(),
),
items: UserRole.values.map((role) {
return DropdownMenuItem<UserRole>(
value: role,
child: Text(role.displayName),
);
}).toList(),
onChanged: (value) {
if (value != null) {
controller.role = value;
}
},
validator: (value) {
if (value == null) {
return '권한을 선택해주세요';
}
return null;
},
),
const SizedBox(height: 4),
Text(
'권한 설명:\n'
'• 관리자: 전체 시스템 관리 및 모든 기능 접근\n'
'• 매니저: 중간 관리 기능 및 승인 권한\n'
'• 직원: 기본 사용 기능만 접근 가능',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
);