Add API contract Dart models

This commit is contained in:
Codex
2026-07-02 13:00:17 +09:00
parent d890893966
commit 6d05f36dcc
10 changed files with 864 additions and 6 deletions
+23
View File
@@ -0,0 +1,23 @@
class ApiError {
const ApiError({
required this.error,
required this.code,
required this.details,
});
final String error;
final String code;
final Map<String, dynamic> details;
factory ApiError.fromJson(Map<String, dynamic> json) {
return ApiError(
error: json['error'] as String? ?? '',
code: json['code'] as String? ?? '',
details: json['details'] as Map<String, dynamic>? ?? {},
);
}
Map<String, dynamic> toJson() {
return {'error': error, 'code': code, 'details': details};
}
}
@@ -0,0 +1,238 @@
import '../../directory/domain/employee.dart';
class LoginDeviceInfo {
const LoginDeviceInfo({
required this.platform,
required this.appVersion,
required this.deviceName,
});
final String platform;
final String appVersion;
final String deviceName;
factory LoginDeviceInfo.fromJson(Map<String, dynamic> json) {
return LoginDeviceInfo(
platform: json['platform'] as String? ?? '',
appVersion: json['appVersion'] as String? ?? '',
deviceName: json['deviceName'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'platform': platform,
'appVersion': appVersion,
'deviceName': deviceName,
};
}
}
class PhoneLoginRequest {
const PhoneLoginRequest({required this.phoneNumber, required this.device});
final String phoneNumber;
final LoginDeviceInfo device;
Map<String, dynamic> toJson() {
return {'phoneNumber': phoneNumber, 'device': device.toJson()};
}
}
class LoginUser {
const LoginUser({
required this.id,
required this.name,
required this.phoneNumber,
required this.tenantId,
required this.tenantName,
required this.tenantSlug,
this.department,
this.grade,
this.position,
this.jobTitle,
});
final String id;
final String name;
final String phoneNumber;
final String tenantId;
final String tenantName;
final String tenantSlug;
final String? department;
final String? grade;
final String? position;
final String? jobTitle;
factory LoginUser.fromJson(Map<String, dynamic> json) {
return LoginUser(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
phoneNumber: json['phoneNumber'] as String? ?? '',
tenantId: json['tenantId'] as String? ?? '',
tenantName: json['tenantName'] as String? ?? '',
tenantSlug: json['tenantSlug'] as String? ?? '',
department: json['department'] as String?,
grade: json['grade'] as String?,
position: json['position'] as String?,
jobTitle: json['jobTitle'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'phoneNumber': phoneNumber,
'tenantId': tenantId,
'tenantName': tenantName,
'tenantSlug': tenantSlug,
'department': department,
'grade': grade,
'position': position,
'jobTitle': jobTitle,
};
}
}
class PhoneLoginResponse {
const PhoneLoginResponse({
required this.status,
required this.token,
required this.expiresAt,
required this.user,
});
final String status;
final String token;
final DateTime expiresAt;
final LoginUser user;
factory PhoneLoginResponse.fromJson(Map<String, dynamic> json) {
return PhoneLoginResponse(
status: json['status'] as String? ?? '',
token: json['token'] as String? ?? '',
expiresAt: DateTime.parse(json['expiresAt'] as String),
user: LoginUser.fromJson(json['user'] as Map<String, dynamic>? ?? {}),
);
}
Map<String, dynamic> toJson() {
return {
'status': status,
'token': token,
'expiresAt': expiresAt.toUtc().toIso8601String(),
'user': user.toJson(),
};
}
}
class UserPermissions {
const UserPermissions({
required this.directory,
required this.organization,
required this.favoritesSync,
});
final bool directory;
final bool organization;
final bool favoritesSync;
factory UserPermissions.fromJson(Map<String, dynamic> json) {
return UserPermissions(
directory: json['directory'] as bool? ?? false,
organization: json['organization'] as bool? ?? false,
favoritesSync: json['favoritesSync'] as bool? ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'directory': directory,
'organization': organization,
'favoritesSync': favoritesSync,
};
}
}
class CurrentUser {
const CurrentUser({
required this.id,
required this.name,
required this.phoneNumber,
required this.email,
required this.tenantId,
required this.tenantName,
required this.tenantSlug,
required this.permissions,
this.department,
this.grade,
this.position,
this.jobTitle,
});
final String id;
final String name;
final String phoneNumber;
final String email;
final String tenantId;
final String tenantName;
final String tenantSlug;
final UserPermissions permissions;
final String? department;
final String? grade;
final String? position;
final String? jobTitle;
factory CurrentUser.fromJson(Map<String, dynamic> json) {
return CurrentUser(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
phoneNumber: json['phoneNumber'] as String? ?? '',
email: json['email'] as String? ?? '',
tenantId: json['tenantId'] as String? ?? '',
tenantName: json['tenantName'] as String? ?? '',
tenantSlug: json['tenantSlug'] as String? ?? '',
permissions: UserPermissions.fromJson(
json['permissions'] as Map<String, dynamic>? ?? {},
),
department: json['department'] as String?,
grade: json['grade'] as String?,
position: json['position'] as String?,
jobTitle: json['jobTitle'] as String?,
);
}
Employee toEmployee() {
return Employee(
id: id,
name: name,
phoneNumber: phoneNumber,
email: email,
tenantId: tenantId,
tenantName: tenantName,
tenantSlug: tenantSlug,
department: department,
grade: grade,
position: position,
jobTitle: jobTitle,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'phoneNumber': phoneNumber,
'email': email,
'tenantId': tenantId,
'tenantName': tenantName,
'tenantSlug': tenantSlug,
'department': department,
'grade': grade,
'position': position,
'jobTitle': jobTitle,
'permissions': permissions.toJson(),
};
}
}
@@ -0,0 +1,233 @@
class Employee {
const Employee({
required this.id,
required this.name,
required this.phoneNumber,
this.phoneDisplay,
this.email,
required this.tenantId,
required this.tenantName,
required this.tenantSlug,
this.department,
this.grade,
this.position,
this.jobTitle,
this.status,
this.profileImageUrl,
this.sortOrder,
});
final String id;
final String name;
final String phoneNumber;
final String? phoneDisplay;
final String? email;
final String tenantId;
final String tenantName;
final String tenantSlug;
final String? department;
final String? grade;
final String? position;
final String? jobTitle;
final String? status;
final String? profileImageUrl;
final int? sortOrder;
factory Employee.fromJson(Map<String, dynamic> json) {
return Employee(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
phoneNumber: json['phoneNumber'] as String? ?? '',
phoneDisplay: json['phoneDisplay'] as String?,
email: json['email'] as String?,
tenantId: json['tenantId'] as String? ?? '',
tenantName: json['tenantName'] as String? ?? '',
tenantSlug: json['tenantSlug'] as String? ?? '',
department: json['department'] as String?,
grade: json['grade'] as String?,
position: json['position'] as String?,
jobTitle: json['jobTitle'] as String?,
status: json['status'] as String?,
profileImageUrl: json['profileImageUrl'] as String?,
sortOrder: json['sortOrder'] as int?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'phoneNumber': phoneNumber,
'phoneDisplay': phoneDisplay,
'email': email,
'tenantId': tenantId,
'tenantName': tenantName,
'tenantSlug': tenantSlug,
'department': department,
'grade': grade,
'position': position,
'jobTitle': jobTitle,
'status': status,
'profileImageUrl': profileImageUrl,
'sortOrder': sortOrder,
};
}
}
class TenantRef {
const TenantRef({
required this.id,
required this.name,
required this.slug,
required this.type,
this.parentId,
});
final String id;
final String name;
final String slug;
final String type;
final String? parentId;
factory TenantRef.fromJson(Map<String, dynamic> json) {
return TenantRef(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
slug: json['slug'] as String? ?? '',
type: json['type'] as String? ?? '',
parentId: json['parentId'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'slug': slug,
'type': type,
'parentId': parentId,
};
}
}
class EmployeeActions {
const EmployeeActions({
required this.call,
required this.sms,
required this.email,
});
final bool call;
final bool sms;
final bool email;
factory EmployeeActions.fromJson(Map<String, dynamic> json) {
return EmployeeActions(
call: json['call'] as bool? ?? false,
sms: json['sms'] as bool? ?? false,
email: json['email'] as bool? ?? false,
);
}
Map<String, dynamic> toJson() {
return {'call': call, 'sms': sms, 'email': email};
}
}
class EmployeeDetail extends Employee {
const EmployeeDetail({
required super.id,
required super.name,
required super.phoneNumber,
super.phoneDisplay,
super.email,
required super.tenantId,
required super.tenantName,
required super.tenantSlug,
super.department,
super.grade,
super.position,
super.jobTitle,
super.status,
super.profileImageUrl,
super.sortOrder,
required this.joinedTenants,
required this.actions,
});
final List<TenantRef> joinedTenants;
final EmployeeActions actions;
factory EmployeeDetail.fromJson(Map<String, dynamic> json) {
return EmployeeDetail(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
phoneNumber: json['phoneNumber'] as String? ?? '',
phoneDisplay: json['phoneDisplay'] as String?,
email: json['email'] as String?,
tenantId: json['tenantId'] as String? ?? '',
tenantName: json['tenantName'] as String? ?? '',
tenantSlug: json['tenantSlug'] as String? ?? '',
department: json['department'] as String?,
grade: json['grade'] as String?,
position: json['position'] as String?,
jobTitle: json['jobTitle'] as String?,
status: json['status'] as String?,
profileImageUrl: json['profileImageUrl'] as String?,
sortOrder: json['sortOrder'] as int?,
joinedTenants: (json['joinedTenants'] as List<dynamic>? ?? [])
.map((item) => TenantRef.fromJson(item as Map<String, dynamic>))
.toList(),
actions: EmployeeActions.fromJson(
json['actions'] as Map<String, dynamic>? ?? {},
),
);
}
@override
Map<String, dynamic> toJson() {
return {
...super.toJson(),
'joinedTenants': joinedTenants.map((tenant) => tenant.toJson()).toList(),
'actions': actions.toJson(),
};
}
}
class EmployeeListResponse {
const EmployeeListResponse({
required this.items,
required this.limit,
required this.offset,
required this.total,
required this.nextCursor,
});
final List<Employee> items;
final int limit;
final int offset;
final int total;
final String nextCursor;
factory EmployeeListResponse.fromJson(Map<String, dynamic> json) {
return EmployeeListResponse(
items: (json['items'] as List<dynamic>? ?? [])
.map((item) => Employee.fromJson(item as Map<String, dynamic>))
.toList(),
limit: json['limit'] as int? ?? 0,
offset: json['offset'] as int? ?? 0,
total: json['total'] as int? ?? 0,
nextCursor: json['nextCursor'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'items': items.map((employee) => employee.toJson()).toList(),
'limit': limit,
'offset': offset,
'total': total,
'nextCursor': nextCursor,
};
}
}
@@ -0,0 +1,20 @@
class FavoriteEmployee {
const FavoriteEmployee({required this.employeeId, required this.createdAt});
final String employeeId;
final DateTime createdAt;
factory FavoriteEmployee.fromJson(Map<String, dynamic> json) {
return FavoriteEmployee(
employeeId: json['employeeId'] as String? ?? '',
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'employeeId': employeeId,
'createdAt': createdAt.toUtc().toIso8601String(),
};
}
}
@@ -0,0 +1,130 @@
import '../../directory/domain/employee.dart';
class TenantSummary {
const TenantSummary({
required this.id,
required this.name,
required this.slug,
required this.type,
this.parentId,
required this.memberCount,
required this.totalMemberCount,
});
final String id;
final String name;
final String slug;
final String type;
final String? parentId;
final int memberCount;
final int totalMemberCount;
factory TenantSummary.fromJson(Map<String, dynamic> json) {
return TenantSummary(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
slug: json['slug'] as String? ?? '',
type: json['type'] as String? ?? '',
parentId: json['parentId'] as String?,
memberCount: json['memberCount'] as int? ?? 0,
totalMemberCount: json['totalMemberCount'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'slug': slug,
'type': type,
'parentId': parentId,
'memberCount': memberCount,
'totalMemberCount': totalMemberCount,
};
}
}
class TenantListResponse {
const TenantListResponse({required this.items, required this.generatedAt});
final List<TenantSummary> items;
final DateTime generatedAt;
factory TenantListResponse.fromJson(Map<String, dynamic> json) {
return TenantListResponse(
items: (json['items'] as List<dynamic>? ?? [])
.map((item) => TenantSummary.fromJson(item as Map<String, dynamic>))
.toList(),
generatedAt: DateTime.parse(json['generatedAt'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'items': items.map((tenant) => tenant.toJson()).toList(),
'generatedAt': generatedAt.toUtc().toIso8601String(),
};
}
}
class OrgChartCacheInfo {
const OrgChartCacheInfo({
required this.source,
required this.hit,
this.ttlSeconds,
});
final String source;
final bool hit;
final int? ttlSeconds;
factory OrgChartCacheInfo.fromJson(Map<String, dynamic> json) {
return OrgChartCacheInfo(
source: json['source'] as String? ?? '',
hit: json['hit'] as bool? ?? false,
ttlSeconds: json['ttlSeconds'] as int?,
);
}
Map<String, dynamic> toJson() {
return {'source': source, 'hit': hit, 'ttlSeconds': ttlSeconds};
}
}
class OrgChartSnapshot {
const OrgChartSnapshot({
required this.tenants,
required this.employees,
required this.generatedAt,
this.cache,
});
final List<TenantSummary> tenants;
final List<Employee> employees;
final DateTime generatedAt;
final OrgChartCacheInfo? cache;
factory OrgChartSnapshot.fromJson(Map<String, dynamic> json) {
return OrgChartSnapshot(
tenants: (json['tenants'] as List<dynamic>? ?? [])
.map((item) => TenantSummary.fromJson(item as Map<String, dynamic>))
.toList(),
employees: (json['employees'] as List<dynamic>? ?? [])
.map((item) => Employee.fromJson(item as Map<String, dynamic>))
.toList(),
generatedAt: DateTime.parse(json['generatedAt'] as String),
cache: json['cache'] == null
? null
: OrgChartCacheInfo.fromJson(json['cache'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() {
return {
'tenants': tenants.map((tenant) => tenant.toJson()).toList(),
'employees': employees.map((employee) => employee.toJson()).toList(),
'generatedAt': generatedAt.toUtc().toIso8601String(),
'cache': cache?.toJson(),
};
}
}
+15
View File
@@ -0,0 +1,15 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tdc114plus/src/core/network/api_error.dart';
void main() {
test('ApiError parses common error payload', () {
final error = ApiError.fromJson({
'error': 'Unauthorized',
'code': 'unauthorized',
'details': {'reason': 'expired'},
});
expect(error.code, 'unauthorized');
expect(error.details['reason'], 'expired');
});
}
+74
View File
@@ -0,0 +1,74 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tdc114plus/src/features/auth/domain/auth_models.dart';
void main() {
test('PhoneLoginRequest serializes phone login payload', () {
const request = PhoneLoginRequest(
phoneNumber: '01012345678',
device: LoginDeviceInfo(
platform: 'android',
appVersion: '0.1.0',
deviceName: 'Pixel 8',
),
);
expect(request.toJson(), {
'phoneNumber': '01012345678',
'device': {
'platform': 'android',
'appVersion': '0.1.0',
'deviceName': 'Pixel 8',
},
});
});
test('PhoneLoginResponse parses session token response', () {
final response = PhoneLoginResponse.fromJson({
'status': 'ok',
'token': 'baron-sso-session-token',
'expiresAt': '2026-07-02T12:00:00Z',
'user': {
'id': 'user-uuid',
'name': 'User One',
'phoneNumber': '+821012345678',
'tenantId': 'tenant-uuid',
'tenantName': 'Hanmac',
'tenantSlug': 'hanmac',
'department': 'Engineering',
'grade': 'Lead',
'position': 'Manager',
'jobTitle': 'Developer',
},
});
expect(response.status, 'ok');
expect(response.token, 'baron-sso-session-token');
expect(
response.expiresAt.toUtc().toIso8601String(),
'2026-07-02T12:00:00.000Z',
);
expect(response.user.tenantSlug, 'hanmac');
});
test('CurrentUser parses permissions and can map to employee', () {
final user = CurrentUser.fromJson({
'id': 'user-uuid',
'name': 'User One',
'phoneNumber': '+821012345678',
'email': 'user@example.com',
'tenantId': 'tenant-uuid',
'tenantName': 'Hanmac',
'tenantSlug': 'hanmac',
'department': 'Engineering',
'permissions': {
'directory': true,
'organization': true,
'favoritesSync': false,
},
});
expect(user.permissions.directory, isTrue);
expect(user.permissions.favoritesSync, isFalse);
expect(user.toEmployee().department, 'Engineering');
});
}
@@ -0,0 +1,63 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tdc114plus/src/features/directory/domain/employee.dart';
void main() {
test('EmployeeListResponse parses paginated employee list', () {
final response = EmployeeListResponse.fromJson({
'items': [
{
'id': 'user-uuid',
'name': 'User One',
'phoneNumber': '+821012345678',
'phoneDisplay': '010-1234-5678',
'email': 'user@example.com',
'tenantId': 'tenant-uuid',
'tenantName': 'Hanmac',
'tenantSlug': 'hanmac',
'department': 'Engineering',
'grade': 'Lead',
'position': 'Manager',
'jobTitle': 'Developer',
'status': 'active',
'profileImageUrl': null,
'sortOrder': 100,
},
],
'limit': 50,
'offset': 0,
'total': 1,
'nextCursor': '',
});
expect(response.items, hasLength(1));
expect(response.items.first.phoneDisplay, '010-1234-5678');
expect(response.total, 1);
});
test('EmployeeDetail parses joined tenants and available actions', () {
final detail = EmployeeDetail.fromJson({
'id': 'user-uuid',
'name': 'User One',
'phoneNumber': '+821012345678',
'email': 'user@example.com',
'tenantId': 'tenant-uuid',
'tenantName': 'Hanmac',
'tenantSlug': 'hanmac',
'joinedTenants': [
{
'id': 'tenant-uuid',
'name': 'Hanmac',
'slug': 'hanmac',
'type': 'COMPANY',
'parentId': 'root-tenant-uuid',
},
],
'status': 'active',
'actions': {'call': true, 'sms': true, 'email': true},
});
expect(detail.joinedTenants.first.type, 'COMPANY');
expect(detail.actions.call, isTrue);
expect(detail.actions.sms, isTrue);
});
}
@@ -0,0 +1,62 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tdc114plus/src/features/organization/domain/organization_models.dart';
void main() {
test('TenantListResponse parses organization filter tenants', () {
final response = TenantListResponse.fromJson({
'items': [
{
'id': 'tenant-uuid',
'name': 'Hanmac',
'slug': 'hanmac',
'type': 'COMPANY',
'parentId': 'family-root',
'memberCount': 120,
'totalMemberCount': 350,
},
],
'generatedAt': '2026-07-02T12:00:00Z',
});
expect(response.items.first.slug, 'hanmac');
expect(response.items.first.totalMemberCount, 350);
});
test('OrgChartSnapshot parses tenants, employees, and cache metadata', () {
final snapshot = OrgChartSnapshot.fromJson({
'tenants': [
{
'id': 'tenant-uuid',
'name': 'Engineering',
'slug': 'engineering',
'type': 'ORGANIZATION',
'parentId': 'company-tenant-uuid',
'memberCount': 12,
'totalMemberCount': 38,
},
],
'employees': [
{
'id': 'user-uuid',
'name': 'User One',
'phoneNumber': '+821012345678',
'phoneDisplay': '010-1234-5678',
'tenantId': 'tenant-uuid',
'tenantName': 'Engineering',
'tenantSlug': 'engineering',
'department': 'Engineering',
'grade': 'Lead',
'position': 'Manager',
'jobTitle': 'Developer',
'status': 'active',
},
],
'generatedAt': '2026-07-02T12:00:00Z',
'cache': {'source': 'redis', 'hit': true, 'ttlSeconds': 300},
});
expect(snapshot.tenants.first.type, 'ORGANIZATION');
expect(snapshot.employees.first.tenantSlug, 'engineering');
expect(snapshot.cache?.source, 'redis');
});
}
@@ -45,8 +45,8 @@
| 11 | tdc114plus 개발 정책 정리 | 완료 | Baron SSO API 생성 우선 원칙, VS Code 기반 AI 개발 방식, Flutter/플랫폼 구현 원칙 정리 | `docs/tdc114plus-development-policy-2026-07-02.md` | | 11 | tdc114plus 개발 정책 정리 | 완료 | Baron SSO API 생성 우선 원칙, VS Code 기반 AI 개발 방식, Flutter/플랫폼 구현 원칙 정리 | `docs/tdc114plus-development-policy-2026-07-02.md` |
| 12 | API 계약 정리 | 완료 | Baron SSO 로그인 API, orgFront 직원/조직 API 계약 정리 | `docs/tdc114plus-api-contract-2026-07-02.md` | | 12 | API 계약 정리 | 완료 | Baron SSO 로그인 API, orgFront 직원/조직 API 계약 정리 | `docs/tdc114plus-api-contract-2026-07-02.md` |
| 13 | API 계약 검토 및 확정 | 완료 | API 계약 초안의 추가 확인사항 검토 후 1차 구현 기준 확정 | `docs/tdc114plus-api-contract-2026-07-02.md` | | 13 | API 계약 검토 및 확정 | 완료 | API 계약 초안의 추가 확인사항 검토 후 1차 구현 기준 확정 | `docs/tdc114plus-api-contract-2026-07-02.md` |
| 14 | 데이터 모델 설계 | 다음 작업 | 직원, 조직, 가족사, 즐겨찾기 모델 정의 | Dart model | | 14 | 데이터 모델 설계 | 완료 | 직원, 조직, 가족사, 즐겨찾기 모델 정의 | Dart model 및 model test |
| 15 | Mock 데이터 기반 화면 확장 | 대기 | 직원목록, 검색, 가족사 필터, 조직도 화면을 mock 데이터로 우선 구현 | 동작 가능한 UI | | 15 | Mock 데이터 기반 화면 확장 | 다음 작업 | 직원목록, 검색, 가족사 필터, 조직도 화면을 mock 데이터로 우선 구현 | 동작 가능한 UI |
| 16 | Baron SSO 로그인 연동 | 대기 | 전화번호 입력 후 SSO 등록 인원 여부 확인 연동 | 로그인 client/repository | | 16 | Baron SSO 로그인 연동 | 대기 | 전화번호 입력 후 SSO 등록 인원 여부 확인 연동 | 로그인 client/repository |
| 17 | orgFront 데이터 연동 | 대기 | 직원/조직 데이터 API 연동 | directory/organization client | | 17 | orgFront 데이터 연동 | 대기 | 직원/조직 데이터 API 연동 | directory/organization client |
| 18 | 전화/문자 액션 구현 | 대기 | `url_launcher` 기반 전화걸기/문자보내기 구현 | 연락 액션 | | 18 | 전화/문자 액션 구현 | 대기 | `url_launcher` 기반 전화걸기/문자보내기 구현 | 연락 액션 |
@@ -62,15 +62,15 @@
| Phase 2 | 완료 | Baron SSO 참조 기준 확정 및 API 계약 준비 | 공식 `origin/dev` 기준 확인, API 개발 worktree 생성, Baron Safe 참고 정책 확인 | `feature/tdc114plus-api` worktree 생성 | | Phase 2 | 완료 | Baron SSO 참조 기준 확정 및 API 계약 준비 | 공식 `origin/dev` 기준 확인, API 개발 worktree 생성, Baron Safe 참고 정책 확인 | `feature/tdc114plus-api` worktree 생성 |
| Phase 2-1 | 완료 | API 계약 초안 작성 | 개발 정책 확인 후 SSO 로그인 API, orgFront 직원/조직 API, 응답 필드, 오류 정책 정리 | API 계약 초안 작성 완료 | | Phase 2-1 | 완료 | API 계약 초안 작성 | 개발 정책 확인 후 SSO 로그인 API, orgFront 직원/조직 API, 응답 필드, 오류 정책 정리 | API 계약 초안 작성 완료 |
| Phase 2-2 | 완료 | API 계약 검토 및 확정 | token 종류, 전화번호 로그인 보안 수준, 개인정보 마스킹 범위, 즐겨찾기 동기화 여부 확인 | API 계약 확정본 | | Phase 2-2 | 완료 | API 계약 검토 및 확정 | token 종류, 전화번호 로그인 보안 수준, 개인정보 마스킹 범위, 즐겨찾기 동기화 여부 확인 | API 계약 확정본 |
| Phase 2-3 | 다음 | Dart 데이터 모델 설계 | 확정된 API 계약 기준으로 직원, 조직, 로그인, 즐겨찾기 model 정의 | model 초안 | | Phase 2-3 | 완료 | Dart 데이터 모델 설계 | 확정된 API 계약 기준으로 직원, 조직, 로그인, 즐겨찾기 model 정의 | analyze/test 통과 |
| Phase 3 | 이후 | Mock 기반 1차 UI 완성 | 직원목록, 검색, 가족사 필터, 조직도, 상세 화면 구성 | API 없이 화면 흐름 확인 가능 | | Phase 3 | 다음 | Mock 기반 1차 UI 완성 | 직원목록, 검색, 가족사 필터, 조직도, 상세 화면 구성 | API 없이 화면 흐름 확인 가능 |
| Phase 4 | 이후 | 실제 API 연동 | SSO 로그인, orgFront 직원/조직 데이터 연동 | 등록 사용자 로그인 및 직원목록 조회 | | Phase 4 | 이후 | 실제 API 연동 | SSO 로그인, orgFront 직원/조직 데이터 연동 | 등록 사용자 로그인 및 직원목록 조회 |
| Phase 5 | 이후 | 핵심 액션 완성 | 전화걸기, 문자보내기, 즐겨찾기 저장 | 1차 기본 기능 수동 검증 | | Phase 5 | 이후 | 핵심 액션 완성 | 전화걸기, 문자보내기, 즐겨찾기 저장 | 1차 기본 기능 수동 검증 |
| Phase 6 | 이후 | 빌드/배포 준비 | Android debug APK, README 실행법, 잔여 이슈 정리 | APK 빌드 성공 | | Phase 6 | 이후 | 빌드/배포 준비 | Android debug APK, README 실행법, 잔여 이슈 정리 | APK 빌드 성공 |
## 4. 다음 작업 판단 ## 4. 다음 작업 판단
현재 다음 작업은 **Phase 2-3: Dart 데이터 모델 설계**이다. 현재 다음 작업은 **Phase 3: Mock 기반 1차 UI 완성**이다.
공식 Baron SSO `origin/dev` 기준의 API 개발용 worktree는 `/home/ubuntu/workspace/baron-sso-tdc114plus-api`에 생성 완료했다. 기존 `baron-sso` 작업 브랜치는 수정/미추적 파일이 많으므로 직접 merge/rebase하지 않는다. 공식 Baron SSO `origin/dev` 기준의 API 개발용 worktree는 `/home/ubuntu/workspace/baron-sso-tdc114plus-api`에 생성 완료했다. 기존 `baron-sso` 작업 브랜치는 수정/미추적 파일이 많으므로 직접 merge/rebase하지 않는다.
@@ -133,4 +133,4 @@ git -C /home/ubuntu/workspace/tdc114plus log --oneline origin/main -n 10
git -C /home/ubuntu/workspace/tdc114plus status --short --branch git -C /home/ubuntu/workspace/tdc114plus status --short --branch
``` ```
현재 문서 기준 다음 작업은 `Phase 2-3: Dart 데이터 모델 설계`이다. 현재 문서 기준 다음 작업은 `Phase 3: Mock 기반 1차 UI 완성`이다.