From 924ed1239c9a990b4e8a71d391820cbb7e2097d6 Mon Sep 17 00:00:00 2001 From: KwonSunJae <32411719+KwonSunJae@users.noreply.github.com> Date: Wed, 31 Dec 2025 23:50:53 +0900 Subject: [PATCH] Revert "Feature/172" --- docker/mysql/init/04-worker-multitenant.sql | 202 --------- .../core/common/security/JwtService.java | 6 +- .../common/service/AuthenticationService.java | 13 +- .../CloudResourceWorkerController.java | 131 ------ .../dto/CloudResourceWorkerMapResponse.java | 107 ----- .../cloud/entity/CloudResourceWorkerMap.java | 95 ----- .../entity/CloudResourceWorkerMapId.java | 42 -- .../CloudResourceWorkerMapRepository.java | 86 ---- .../service/CloudResourceWorkerService.java | 181 --------- .../service/HealthCheckService.java | 4 +- .../MonitoringNotificationService.java | 6 +- .../controller/OrganizationController.java | 41 +- .../OrganizationMemberController.java | 136 ------- .../controller/TenantWorkerController.java | 138 ------- .../UserOrganizationController.java | 65 --- .../controller/WorkerController.java | 129 ------ .../controller/WorkerRoleController.java | 135 ------ .../organization/dto/AddMemberRequest.java | 41 -- .../organization/dto/AssignRoleRequest.java | 35 -- .../organization/dto/AssignWorkerRequest.java | 41 -- .../organization/dto/CreateWorkerRequest.java | 35 -- .../dto/OrganizationMemberResponse.java | 90 ---- .../dto/OrganizationResponse.java | 28 +- .../dto/TenantWorkerMapResponse.java | 104 ----- .../organization/dto/WorkerResponse.java | 90 ---- .../organization/dto/WorkerRoleResponse.java | 105 ----- .../organization/entity/Organization.java | 100 ++++- .../entity/OrganizationMember.java | 84 ---- .../entity/OrganizationMemberId.java | 42 -- .../organization/entity/TenantWorkerMap.java | 102 ----- .../entity/TenantWorkerMapId.java | 40 -- .../domain/organization/entity/Worker.java | 61 --- .../organization/entity/WorkerRole.java | 90 ---- .../organization/entity/WorkerRoleId.java | 43 -- .../organization/enums/WorkerErrorCode.java | 59 --- .../OrganizationMemberRepository.java | 65 --- .../repository/OrganizationRepository.java | 51 +-- .../repository/TenantWorkerMapRepository.java | 74 ---- .../repository/WorkerRepository.java | 79 ---- .../repository/WorkerRoleRepository.java | 87 ---- ...OrganizationAwareAuthorizationService.java | 30 +- .../service/OrganizationMemberService.java | 127 ------ .../service/OrganizationService.java | 383 +++++++++++++----- .../service/TenantWorkerService.java | 201 --------- .../service/WorkerRoleService.java | 113 ------ .../organization/service/WorkerService.java | 142 ------- .../service/AuthorizationServiceImpl.java | 9 +- .../core/domain/tenant/entity/Tenant.java | 8 +- .../core/domain/user/entity/Permission.java | 8 +- .../core/domain/user/entity/Role.java | 7 +- .../core/domain/user/entity/User.java | 11 + .../user/repository/UserRepository.java | 24 +- .../core/domain/user/service/UserService.java | 35 +- .../service}/HealthCheckServiceTest.java | 0 .../MonitoringNotificationServiceTest.java | 0 .../OrganizationMemberControllerTest.java | 148 ------- .../TenantWorkerControllerTest.java | 178 -------- .../UserOrganizationControllerTest.java | 116 ------ .../controller/WorkerControllerTest.java | 163 -------- .../controller/WorkerRoleControllerTest.java | 179 -------- ...nizationAwareAuthorizationServiceTest.java | 0 .../OrganizationHierarchyServiceTest.java | 0 .../service}/OrganizationRoleServiceTest.java | 0 .../service}/OrganizationServiceTest.java | 0 .../OrganizationTenantServiceTest.java | 0 .../service/WorkerServiceIntegrationTest.java | 327 --------------- .../AuthorizationServiceImplTest.java | 0 67 files changed, 495 insertions(+), 4777 deletions(-) delete mode 100644 docker/mysql/init/04-worker-multitenant.sql delete mode 100644 src/main/java/com/agenticcp/core/domain/cloud/controller/CloudResourceWorkerController.java delete mode 100644 src/main/java/com/agenticcp/core/domain/cloud/dto/CloudResourceWorkerMapResponse.java delete mode 100644 src/main/java/com/agenticcp/core/domain/cloud/entity/CloudResourceWorkerMap.java delete mode 100644 src/main/java/com/agenticcp/core/domain/cloud/entity/CloudResourceWorkerMapId.java delete mode 100644 src/main/java/com/agenticcp/core/domain/cloud/repository/CloudResourceWorkerMapRepository.java delete mode 100644 src/main/java/com/agenticcp/core/domain/cloud/service/CloudResourceWorkerService.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/controller/OrganizationMemberController.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/controller/TenantWorkerController.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/controller/UserOrganizationController.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/controller/WorkerController.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/controller/WorkerRoleController.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/dto/AddMemberRequest.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/dto/AssignRoleRequest.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/dto/AssignWorkerRequest.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/dto/CreateWorkerRequest.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/dto/OrganizationMemberResponse.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/dto/TenantWorkerMapResponse.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/dto/WorkerResponse.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/dto/WorkerRoleResponse.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/entity/OrganizationMember.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/entity/OrganizationMemberId.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/entity/TenantWorkerMap.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/entity/TenantWorkerMapId.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/entity/Worker.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/entity/WorkerRole.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/entity/WorkerRoleId.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/enums/WorkerErrorCode.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/repository/OrganizationMemberRepository.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/repository/TenantWorkerMapRepository.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/repository/WorkerRepository.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/repository/WorkerRoleRepository.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/service/OrganizationMemberService.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/service/TenantWorkerService.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/service/WorkerRoleService.java delete mode 100644 src/main/java/com/agenticcp/core/domain/organization/service/WorkerService.java rename src/test/{java-disabled => java/com/agenticcp/core/domain/monitoring/service}/HealthCheckServiceTest.java (100%) rename src/test/{java-disabled => java/com/agenticcp/core/domain/notification/service}/MonitoringNotificationServiceTest.java (100%) delete mode 100644 src/test/java/com/agenticcp/core/domain/organization/controller/OrganizationMemberControllerTest.java delete mode 100644 src/test/java/com/agenticcp/core/domain/organization/controller/TenantWorkerControllerTest.java delete mode 100644 src/test/java/com/agenticcp/core/domain/organization/controller/UserOrganizationControllerTest.java delete mode 100644 src/test/java/com/agenticcp/core/domain/organization/controller/WorkerControllerTest.java delete mode 100644 src/test/java/com/agenticcp/core/domain/organization/controller/WorkerRoleControllerTest.java rename src/test/{java-disabled => java/com/agenticcp/core/domain/organization/service}/OrganizationAwareAuthorizationServiceTest.java (100%) rename src/test/{java-disabled => java/com/agenticcp/core/domain/organization/service}/OrganizationHierarchyServiceTest.java (100%) rename src/test/{java-disabled => java/com/agenticcp/core/domain/organization/service}/OrganizationRoleServiceTest.java (100%) rename src/test/{java-disabled => java/com/agenticcp/core/domain/organization/service}/OrganizationServiceTest.java (100%) rename src/test/{java-disabled => java/com/agenticcp/core/domain/organization/service}/OrganizationTenantServiceTest.java (100%) delete mode 100644 src/test/java/com/agenticcp/core/domain/organization/service/WorkerServiceIntegrationTest.java rename src/test/{java-disabled => java/com/agenticcp/core/domain/security/service}/AuthorizationServiceImplTest.java (100%) diff --git a/docker/mysql/init/04-worker-multitenant.sql b/docker/mysql/init/04-worker-multitenant.sql deleted file mode 100644 index 48c4452a3..000000000 --- a/docker/mysql/init/04-worker-multitenant.sql +++ /dev/null @@ -1,202 +0,0 @@ --- Worker 기반 멀티 테넌트 구조 마이그레이션 스크립트 --- 이슈 #172: Organization Business 로직 리팩토링 (설계 B 기준) --- --- 주의사항: --- 1. 마이그레이션 전 반드시 데이터 백업 --- 2. 단계별 검증 후 다음 단계 진행 --- 3. 롤백 스크립트 준비 권장 - -USE agenticcp; - --- ========== 1. 새로운 테이블 생성 ========== - --- Worker 테이블 (설계 B 기준) --- User 1:N Worker 관계, (user_id, tenant_id) 복합 Unique 제약 -CREATE TABLE IF NOT EXISTS workers ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - user_id BIGINT NOT NULL, - tenant_id BIGINT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - created_by VARCHAR(100), - updated_by VARCHAR(100), - is_deleted BOOLEAN DEFAULT FALSE, - - CONSTRAINT fk_worker_user FOREIGN KEY (user_id) REFERENCES users(id), - CONSTRAINT fk_worker_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id), - CONSTRAINT uk_worker_user_tenant UNIQUE (user_id, tenant_id) -); - -CREATE INDEX IF NOT EXISTS idx_worker_user ON workers(user_id); -CREATE INDEX IF NOT EXISTS idx_worker_tenant ON workers(tenant_id); -CREATE INDEX IF NOT EXISTS idx_worker_deleted ON workers(is_deleted); - --- OrganizationMember 테이블 (복합 PK) --- (organization_id, user_id)가 복합 PK -CREATE TABLE IF NOT EXISTS organization_member ( - organization_id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - role VARCHAR(50), - joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - PRIMARY KEY (organization_id, user_id), - CONSTRAINT fk_org_member_org FOREIGN KEY (organization_id) REFERENCES organizations(id), - CONSTRAINT fk_org_member_user FOREIGN KEY (user_id) REFERENCES users(id) -); - -CREATE INDEX IF NOT EXISTS idx_org_member_org ON organization_member(organization_id); -CREATE INDEX IF NOT EXISTS idx_org_member_user ON organization_member(user_id); - --- TenantWorkerMap 테이블 (복합 PK) --- Shared Tenant 접근 관리 -CREATE TABLE IF NOT EXISTS tenant_worker_map ( - tenant_id BIGINT NOT NULL, - worker_id BIGINT NOT NULL, - access_scope VARCHAR(30), - joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - created_by VARCHAR(100), - updated_by VARCHAR(100), - is_deleted BOOLEAN DEFAULT FALSE, - - PRIMARY KEY (tenant_id, worker_id), - CONSTRAINT fk_tenant_worker_map_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id), - CONSTRAINT fk_tenant_worker_map_worker FOREIGN KEY (worker_id) REFERENCES workers(id) -); - -CREATE INDEX IF NOT EXISTS idx_tenant_worker_map_tenant ON tenant_worker_map(tenant_id); -CREATE INDEX IF NOT EXISTS idx_tenant_worker_map_worker ON tenant_worker_map(worker_id); -CREATE INDEX IF NOT EXISTS idx_tenant_worker_map_deleted ON tenant_worker_map(is_deleted); - --- WorkerRole 테이블 (복합 PK) --- Worker 역할 관리: (worker_id, role_id, tenant_id)가 복합 PK -CREATE TABLE IF NOT EXISTS worker_role ( - worker_id BIGINT NOT NULL, - role_id BIGINT NOT NULL, - tenant_id BIGINT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - created_by VARCHAR(100), - updated_by VARCHAR(100), - is_deleted BOOLEAN DEFAULT FALSE, - - PRIMARY KEY (worker_id, role_id, tenant_id), - CONSTRAINT fk_worker_role_worker FOREIGN KEY (worker_id) REFERENCES workers(id), - CONSTRAINT fk_worker_role_role FOREIGN KEY (role_id) REFERENCES roles(id), - CONSTRAINT fk_worker_role_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) -); - -CREATE INDEX IF NOT EXISTS idx_worker_role_worker ON worker_role(worker_id); -CREATE INDEX IF NOT EXISTS idx_worker_role_tenant ON worker_role(tenant_id); -CREATE INDEX IF NOT EXISTS idx_worker_role_role ON worker_role(role_id); -CREATE INDEX IF NOT EXISTS idx_worker_role_deleted ON worker_role(is_deleted); - --- ========== 2. Tenant 테이블 수정 ========== --- 설계 B: Organization ↔ Tenant 1:1 관계 --- tenant_type은 이미 존재하므로 유지 --- owner_org_id는 제거 (1:1 관계로 organization_id로 관리) - --- tenant_type 컬럼이 없으면 추가 --- MySQL 8.0.19 이전 버전 호환을 위해 프로시저 사용 -SET @dbname = DATABASE(); -SET @tablename = 'tenants'; -SET @columnname = 'tenant_type'; -SET @preparedStatement = (SELECT IF( - ( - SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS - WHERE - (TABLE_SCHEMA = @dbname) - AND (TABLE_NAME = @tablename) - AND (COLUMN_NAME = @columnname) - ) > 0, - 'SELECT 1', - CONCAT('ALTER TABLE ', @tablename, ' ADD COLUMN ', @columnname, ' VARCHAR(20) CHECK (tenant_type IN (''DEDICATED'',''SHARED''))') -)); -PREPARE alterIfNotExists FROM @preparedStatement; -EXECUTE alterIfNotExists; -DEALLOCATE PREPARE alterIfNotExists; - --- ========== 3. 기존 데이터 마이그레이션 ========== - --- 3.1. User.tenant_id를 기반으로 Worker 생성 --- User 1:N Worker 관계 구현 --- 주의: User.tenant_id가 NULL인 경우는 제외 -INSERT INTO workers (user_id, tenant_id, created_at, updated_at, created_by) -SELECT - id AS user_id, - tenant_id, - created_at, - updated_at, - created_by -FROM users -WHERE tenant_id IS NOT NULL -ON DUPLICATE KEY UPDATE - updated_at = CURRENT_TIMESTAMP; - --- 3.2. User.organization_id를 organization_member로 이관 --- 주의: User.organization_id가 NULL인 경우는 제외 -INSERT INTO organization_member (organization_id, user_id, role, joined_at, created_at, updated_at) -SELECT - organization_id, - id AS user_id, - NULL AS role, -- 기존 데이터에 role 정보가 없으므로 NULL - created_at AS joined_at, - created_at, - updated_at -FROM users -WHERE organization_id IS NOT NULL -ON DUPLICATE KEY UPDATE - updated_at = CURRENT_TIMESTAMP; - --- 3.3. Dedicated Tenant의 경우 Worker가 자동으로 소속되므로 별도 작업 불필요 --- Shared Tenant의 경우 TenantWorkerMap은 수동으로 할당해야 함 - --- ========== 4. 데이터 검증 쿼리 ========== - --- Worker 생성 확인 -SELECT - 'Workers created' AS status, - COUNT(*) AS count -FROM workers; - --- OrganizationMember 생성 확인 -SELECT - 'OrganizationMembers created' AS status, - COUNT(*) AS count -FROM organization_member; - --- User별 Worker 수 확인 -SELECT - u.id AS user_id, - u.username, - COUNT(w.id) AS worker_count -FROM users u -LEFT JOIN workers w ON u.id = w.user_id -GROUP BY u.id, u.username -ORDER BY worker_count DESC; - --- Tenant별 Worker 수 확인 -SELECT - t.id AS tenant_id, - t.tenant_key, - t.tenant_type, - COUNT(w.id) AS worker_count -FROM tenants t -LEFT JOIN workers w ON t.id = w.tenant_id -GROUP BY t.id, t.tenant_key, t.tenant_type -ORDER BY worker_count DESC; - --- ========== 5. 롤백 스크립트 (참고용) ========== --- 주의: 실제 롤백 시에는 데이터 백업에서 복원하는 것을 권장 - -/* --- 롤백 순서 (역순) -DROP TABLE IF EXISTS worker_role; -DROP TABLE IF EXISTS tenant_worker_map; -DROP TABLE IF EXISTS organization_member; -DROP TABLE IF EXISTS workers; -*/ - diff --git a/src/main/java/com/agenticcp/core/common/security/JwtService.java b/src/main/java/com/agenticcp/core/common/security/JwtService.java index d9f5b5e17..75d409d01 100644 --- a/src/main/java/com/agenticcp/core/common/security/JwtService.java +++ b/src/main/java/com/agenticcp/core/common/security/JwtService.java @@ -51,10 +51,8 @@ public String generateAccessToken(User user) { claims.put(CLAIM_USERNAME, user.getUsername()); claims.put(CLAIM_EMAIL, user.getEmail()); claims.put(CLAIM_ROLE, user.getRole().name()); - // 설계 B: User는 전역 계정이므로 tenant 정보는 Worker를 통해 가져와야 함 - // TODO: 현재 활성 테넌트 컨텍스트에서 가져오거나 Worker 목록에서 선택 - claims.put(CLAIM_TENANT_ID, null); // TODO: Worker를 통해 현재 테넌트 정보 가져오기 - claims.put(CLAIM_TENANT_KEY, null); // TODO: Worker를 통해 현재 테넌트 정보 가져오기 + claims.put(CLAIM_TENANT_ID, user.getTenant() != null ? user.getTenant().getId() : null); + claims.put(CLAIM_TENANT_KEY, user.getTenant() != null ? user.getTenant().getTenantKey() : null); List permissions = getUserPermissions(user); if (!permissions.isEmpty()) { diff --git a/src/main/java/com/agenticcp/core/common/service/AuthenticationService.java b/src/main/java/com/agenticcp/core/common/service/AuthenticationService.java index 90ad342d9..e123aba46 100644 --- a/src/main/java/com/agenticcp/core/common/service/AuthenticationService.java +++ b/src/main/java/com/agenticcp/core/common/service/AuthenticationService.java @@ -99,7 +99,6 @@ public TokenResponse register(RegisterRequest request, HttpServletRequest httpRe boolean twoFactorRequired = true; // 기본값: 2FA 필수 Status initialStatus = twoFactorRequired ? Status.PENDING : Status.ACTIVE; - // 설계 B: User는 전역 계정이므로 tenant 필드 제거 User newUser = User.builder() .username(request.getUsername()) .email(request.getEmail()) @@ -107,12 +106,8 @@ public TokenResponse register(RegisterRequest request, HttpServletRequest httpRe .name(request.getName()) .role(UserRole.VIEWER) // 기본 역할 부여 .status(initialStatus) // 2FA 정책에 따라 PENDING 또는 ACTIVE + .tenant(tenant) .build(); - - // TODO: 설계 B - 테넌트가 제공된 경우 Worker를 생성해야 함 - // if (tenant != null) { - // workerService.createWorker(newUser.getId(), tenant.getId()); - // } savedUser = userService.saveUser(newUser); log.info("[AuthenticationService] register - User registered successfully: {}", savedUser.getUsername()); @@ -368,15 +363,13 @@ public UserInfoResponse getCurrentUser(String username) { // 권한 목록 추출 (임시로 빈 리스트) List permissions = List.of(); - // 설계 B: User는 전역 계정이므로 tenant 정보는 Worker를 통해 가져와야 함 - // TODO: 현재 활성 테넌트 컨텍스트에서 가져오거나 Worker 목록에서 선택 return UserInfoResponse.builder() .username(user.getUsername()) .email(user.getEmail()) .name(user.getName()) .role(user.getRole().name()) - .tenantId(null) // TODO: Worker를 통해 현재 테넌트 정보 가져오기 - .tenantKey(null) // TODO: Worker를 통해 현재 테넌트 정보 가져오기 + .tenantId(user.getTenant() != null ? user.getTenant().getId() : null) + .tenantKey(user.getTenant() != null ? user.getTenant().getTenantKey() : null) .permissions(permissions) .lastLogin(user.getLastLogin()) .twoFactorEnabled(user.getTwoFactorEnabled()) diff --git a/src/main/java/com/agenticcp/core/domain/cloud/controller/CloudResourceWorkerController.java b/src/main/java/com/agenticcp/core/domain/cloud/controller/CloudResourceWorkerController.java deleted file mode 100644 index a1710eca3..000000000 --- a/src/main/java/com/agenticcp/core/domain/cloud/controller/CloudResourceWorkerController.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.agenticcp.core.domain.cloud.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.cloud.dto.CloudResourceWorkerMapResponse; -import com.agenticcp.core.domain.cloud.service.CloudResourceWorkerService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.Positive; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * 클라우드 리소스-Worker 관리 컨트롤러 - * - *

클라우드 리소스와 Worker 간의 관계를 관리하는 API입니다. - * 설계 C 기준: 리소스 단위로 Worker 접근 권한을 관리합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-19 - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/cloud-resources/{resourceId}/workers") -@RequiredArgsConstructor -@Tag(name = "Cloud Resource Worker Management", description = "클라우드 리소스-Worker 관리 API") -public class CloudResourceWorkerController { - - private final CloudResourceWorkerService cloudResourceWorkerService; - - /** - * 클라우드 리소스의 Worker 목록 조회 - * - * @param resourceId 클라우드 리소스 ID - * @return CloudResourceWorkerMap 목록 - */ - @GetMapping - @Operation( - summary = "클라우드 리소스의 Worker 목록 조회", - description = "특정 클라우드 리소스에 할당된 Worker 목록을 조회합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공", - content = @Content(schema = @Schema(implementation = CloudResourceWorkerMapResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "클라우드 리소스를 찾을 수 없음") - }) - public ResponseEntity>> getWorkers( - @Parameter(description = "클라우드 리소스 ID", required = true, example = "1") - @PathVariable @Positive Long resourceId) { - log.info("[CloudResourceWorkerController] getWorkers - resourceId={}", resourceId); - - List responses = cloudResourceWorkerService.findByResourceId(resourceId) - .stream() - .map(CloudResourceWorkerMapResponse::from) - .collect(Collectors.toList()); - - return ResponseEntity.ok(ApiResponse.success(responses, "Worker 목록을 성공적으로 조회했습니다.")); - } - - /** - * 클라우드 리소스에 Worker 할당 - * - * @param resourceId 클라우드 리소스 ID - * @param workerId Worker ID - * @return 할당된 CloudResourceWorkerMap 정보 - */ - @PostMapping("/{workerId}") - @Operation( - summary = "클라우드 리소스에 Worker 할당", - description = "클라우드 리소스에 Worker를 할당합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "Worker 할당 성공", - content = @Content(schema = @Schema(implementation = CloudResourceWorkerMapResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "클라우드 리소스 또는 Worker를 찾을 수 없음"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 할당된 Worker") - }) - public ResponseEntity> assignWorker( - @Parameter(description = "클라우드 리소스 ID", required = true, example = "1") - @PathVariable @Positive Long resourceId, - @Parameter(description = "Worker ID", required = true, example = "1") - @PathVariable @Positive Long workerId) { - log.info("[CloudResourceWorkerController] assignWorker - resourceId={}, workerId={}", - resourceId, workerId); - - CloudResourceWorkerMapResponse response = CloudResourceWorkerMapResponse.from( - cloudResourceWorkerService.assignWorkerToResource(resourceId, workerId)); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(response, "Worker가 성공적으로 할당되었습니다.")); - } - - /** - * 클라우드 리소스에서 Worker 제거 - * - * @param resourceId 클라우드 리소스 ID - * @param workerId Worker ID - */ - @DeleteMapping("/{workerId}") - @Operation( - summary = "클라우드 리소스에서 Worker 제거", - description = "클라우드 리소스에서 Worker를 제거합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "204", description = "Worker 제거 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "클라우드 리소스 또는 Worker를 찾을 수 없음") - }) - public ResponseEntity removeWorker( - @Parameter(description = "클라우드 리소스 ID", required = true, example = "1") - @PathVariable @Positive Long resourceId, - @Parameter(description = "Worker ID", required = true, example = "1") - @PathVariable @Positive Long workerId) { - log.info("[CloudResourceWorkerController] removeWorker - resourceId={}, workerId={}", - resourceId, workerId); - - cloudResourceWorkerService.removeWorkerFromResource(resourceId, workerId); - - return ResponseEntity.noContent().build(); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/cloud/dto/CloudResourceWorkerMapResponse.java b/src/main/java/com/agenticcp/core/domain/cloud/dto/CloudResourceWorkerMapResponse.java deleted file mode 100644 index 3db3bc418..000000000 --- a/src/main/java/com/agenticcp/core/domain/cloud/dto/CloudResourceWorkerMapResponse.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.agenticcp.core.domain.cloud.dto; - -import com.agenticcp.core.domain.cloud.entity.CloudResourceWorkerMap; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * CloudResourceWorkerMap 응답 DTO - * - *

클라우드 리소스-Worker 매핑 정보를 표현하는 응답 DTO입니다. - * 설계 C 기준: 리소스 단위로 Worker 접근 권한을 관리합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-19 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -@Schema(description = "클라우드 리소스-Worker 매핑 응답") -public class CloudResourceWorkerMapResponse { - - /** 클라우드 리소스 ID */ - @Schema(description = "클라우드 리소스 ID", example = "1") - private Long cloudResourceId; - - /** 클라우드 리소스명 */ - @Schema(description = "클라우드 리소스명", example = "my-resource") - private String cloudResourceName; - - /** Worker ID */ - @Schema(description = "Worker ID", example = "1") - private Long workerId; - - /** 사용자 ID */ - @Schema(description = "사용자 ID", example = "1") - private Long userId; - - /** 사용자명 */ - @Schema(description = "사용자명", example = "john_doe") - private String username; - - /** 사용자 이메일 */ - @Schema(description = "사용자 이메일", example = "john@example.com") - private String userEmail; - - /** 사용자 이름 */ - @Schema(description = "사용자 이름", example = "John Doe") - private String userName; - - /** 조직 ID */ - @Schema(description = "조직 ID", example = "1") - private Long organizationId; - - /** 조직명 */ - @Schema(description = "조직명", example = "개발팀") - private String organizationName; - - /** 생성일시 */ - @Schema(description = "생성일시", example = "2024-01-01T00:00:00") - private LocalDateTime createdAt; - - /** 수정일시 */ - @Schema(description = "수정일시", example = "2024-01-01T00:00:00") - private LocalDateTime updatedAt; - - /** - * CloudResourceWorkerMap 엔티티를 CloudResourceWorkerMapResponse로 변환 - * - * @param map CloudResourceWorkerMap 엔티티 - * @return CloudResourceWorkerMap 응답 DTO - */ - public static CloudResourceWorkerMapResponse from(CloudResourceWorkerMap map) { - if (map == null) { - return null; - } - - return CloudResourceWorkerMapResponse.builder() - .cloudResourceId(map.getCloudResource() != null ? map.getCloudResource().getId() : null) - .cloudResourceName(map.getCloudResource() != null ? map.getCloudResource().getResourceName() : null) - .workerId(map.getWorker() != null ? map.getWorker().getId() : null) - .userId(map.getWorker() != null && map.getWorker().getUser() != null - ? map.getWorker().getUser().getId() : null) - .username(map.getWorker() != null && map.getWorker().getUser() != null - ? map.getWorker().getUser().getUsername() : null) - .userEmail(map.getWorker() != null && map.getWorker().getUser() != null - ? map.getWorker().getUser().getEmail() : null) - .userName(map.getWorker() != null && map.getWorker().getUser() != null - ? map.getWorker().getUser().getName() : null) - .organizationId(map.getWorker() != null && map.getWorker().getOrganization() != null - ? map.getWorker().getOrganization().getId() : null) - .organizationName(map.getWorker() != null && map.getWorker().getOrganization() != null - ? map.getWorker().getOrganization().getName() : null) - .createdAt(map.getCreatedAt()) - .updatedAt(map.getUpdatedAt()) - .build(); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/cloud/entity/CloudResourceWorkerMap.java b/src/main/java/com/agenticcp/core/domain/cloud/entity/CloudResourceWorkerMap.java deleted file mode 100644 index 8acf9b8fa..000000000 --- a/src/main/java/com/agenticcp/core/domain/cloud/entity/CloudResourceWorkerMap.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.agenticcp.core.domain.cloud.entity; - -import com.agenticcp.core.domain.organization.entity.Worker; -import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -/** - * CloudResourceWorkerMap 엔티티 - * - *

클라우드 리소스와 Worker 간의 매핑을 정의하는 엔티티입니다. - * 설계 C 기준: 리소스 단위로 Worker 접근 권한을 관리합니다. - * access_scope는 불필요합니다 (리소스 단위로 관리하므로).

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Entity -@Table(name = "cloud_resource_worker_map", uniqueConstraints = { - @UniqueConstraint(name = "uk_cloud_resource_worker", columnNames = {"cloud_resource_id", "worker_id"}) -}, indexes = { - @Index(name = "idx_cloud_resource_worker_resource", columnList = "cloud_resource_id"), - @Index(name = "idx_cloud_resource_worker_worker", columnList = "worker_id") -}) -@IdClass(CloudResourceWorkerMapId.class) -@EntityListeners(AuditingEntityListener.class) -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -public class CloudResourceWorkerMap { - - /** - * 클라우드 리소스 (복합 PK의 일부) - */ - @Id - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "cloud_resource_id", nullable = false) - private CloudResource cloudResource; - - /** - * Worker (복합 PK의 일부) - */ - @Id - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "worker_id", nullable = false) - private Worker worker; - - /** - * 생성일시 - */ - @CreatedDate - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - /** - * 수정일시 - */ - @LastModifiedDate - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - /** - * 생성자 - */ - @Column(name = "created_by") - private String createdBy; - - /** - * 수정자 - */ - @Column(name = "updated_by") - private String updatedBy; - - /** - * 삭제 여부 - */ - @Column(name = "is_deleted", nullable = false) - @Builder.Default - private Boolean isDeleted = false; -} - diff --git a/src/main/java/com/agenticcp/core/domain/cloud/entity/CloudResourceWorkerMapId.java b/src/main/java/com/agenticcp/core/domain/cloud/entity/CloudResourceWorkerMapId.java deleted file mode 100644 index 117398825..000000000 --- a/src/main/java/com/agenticcp/core/domain/cloud/entity/CloudResourceWorkerMapId.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.agenticcp.core.domain.cloud.entity; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; -import java.util.Objects; - -/** - * CloudResourceWorkerMap 복합 PK 클래스 - * - *

설계 C 기준: (cloud_resource_id, worker_id) 복합 PK

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-19 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CloudResourceWorkerMapId implements Serializable { - - // JPA @IdClass 사용 시 엔티티의 필드명과 일치해야 함 (cloudResource, worker) - private Long cloudResource; - private Long worker; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CloudResourceWorkerMapId that = (CloudResourceWorkerMapId) o; - return Objects.equals(cloudResource, that.cloudResource) && - Objects.equals(worker, that.worker); - } - - @Override - public int hashCode() { - return Objects.hash(cloudResource, worker); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/cloud/repository/CloudResourceWorkerMapRepository.java b/src/main/java/com/agenticcp/core/domain/cloud/repository/CloudResourceWorkerMapRepository.java deleted file mode 100644 index 158ce978a..000000000 --- a/src/main/java/com/agenticcp/core/domain/cloud/repository/CloudResourceWorkerMapRepository.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.agenticcp.core.domain.cloud.repository; - -import com.agenticcp.core.domain.cloud.entity.CloudResourceWorkerMap; -import com.agenticcp.core.domain.cloud.entity.CloudResourceWorkerMapId; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -/** - * CloudResourceWorkerMap Repository - * - *

CloudResourceWorkerMap 엔티티에 대한 데이터 접근을 제공합니다. - * 설계 C 기준: 리소스 단위로 Worker 접근 권한을 관리합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Repository -public interface CloudResourceWorkerMapRepository extends JpaRepository { - - /** - * 클라우드 리소스 ID로 CloudResourceWorkerMap 목록 조회 - * - * @param resourceId 클라우드 리소스 ID - * @return CloudResourceWorkerMap 목록 - */ - @Query("SELECT cwm FROM CloudResourceWorkerMap cwm WHERE cwm.cloudResource.id = :resourceId AND cwm.isDeleted = false") - List findByResourceId(@Param("resourceId") Long resourceId); - - /** - * Worker ID로 CloudResourceWorkerMap 목록 조회 - * - * @param workerId Worker ID - * @return CloudResourceWorkerMap 목록 - */ - @Query("SELECT cwm FROM CloudResourceWorkerMap cwm WHERE cwm.worker.id = :workerId AND cwm.isDeleted = false") - List findByWorkerId(@Param("workerId") Long workerId); - - /** - * 클라우드 리소스 ID와 Worker ID로 CloudResourceWorkerMap 조회 - * - * @param resourceId 클라우드 리소스 ID - * @param workerId Worker ID - * @return CloudResourceWorkerMap (Optional) - */ - @Query("SELECT cwm FROM CloudResourceWorkerMap cwm WHERE cwm.cloudResource.id = :resourceId AND cwm.worker.id = :workerId AND cwm.isDeleted = false") - Optional findByResourceIdAndWorkerId(@Param("resourceId") Long resourceId, @Param("workerId") Long workerId); - - /** - * 클라우드 리소스 ID와 Worker ID로 CloudResourceWorkerMap 존재 여부 확인 - * - * @param resourceId 클라우드 리소스 ID - * @param workerId Worker ID - * @return 존재 여부 - */ - @Query("SELECT COUNT(cwm) > 0 FROM CloudResourceWorkerMap cwm WHERE cwm.cloudResource.id = :resourceId AND cwm.worker.id = :workerId AND cwm.isDeleted = false") - boolean existsByResourceIdAndWorkerId(@Param("resourceId") Long resourceId, @Param("workerId") Long workerId); - - /** - * 테넌트 ID로 CloudResourceWorkerMap 목록 조회 - * (테넌트의 모든 리소스에 매핑된 Worker 조회) - * - * @param tenantId 테넌트 ID - * @return CloudResourceWorkerMap 목록 - */ - @Query("SELECT cwm FROM CloudResourceWorkerMap cwm " + - "WHERE cwm.cloudResource.tenant.id = :tenantId AND cwm.isDeleted = false") - List findByTenantId(@Param("tenantId") Long tenantId); - - /** - * 클라우드 리소스 ID와 Worker ID로 CloudResourceWorkerMap 삭제 (소프트 삭제) - * - * @param resourceId 클라우드 리소스 ID - * @param workerId Worker ID - */ - @Modifying - @Query("UPDATE CloudResourceWorkerMap cwm SET cwm.isDeleted = true WHERE cwm.cloudResource.id = :resourceId AND cwm.worker.id = :workerId") - void deleteByResourceIdAndWorkerId(@Param("resourceId") Long resourceId, @Param("workerId") Long workerId); -} - diff --git a/src/main/java/com/agenticcp/core/domain/cloud/service/CloudResourceWorkerService.java b/src/main/java/com/agenticcp/core/domain/cloud/service/CloudResourceWorkerService.java deleted file mode 100644 index 2c59dfcd9..000000000 --- a/src/main/java/com/agenticcp/core/domain/cloud/service/CloudResourceWorkerService.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.agenticcp.core.domain.cloud.service; - -import com.agenticcp.core.common.exception.BusinessException; -import com.agenticcp.core.domain.cloud.entity.CloudResource; -import com.agenticcp.core.domain.cloud.entity.CloudResourceWorkerMap; -import com.agenticcp.core.domain.cloud.repository.CloudResourceRepository; -import com.agenticcp.core.domain.cloud.repository.CloudResourceWorkerMapRepository; -import com.agenticcp.core.domain.organization.entity.Worker; -import com.agenticcp.core.domain.organization.enums.WorkerErrorCode; -import com.agenticcp.core.domain.organization.repository.WorkerRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -/** - * CloudResourceWorkerService - * - *

클라우드 리소스와 Worker 간의 관계를 관리하는 서비스입니다. - * 설계 C 기준: 리소스 단위로 Worker 접근 권한을 관리합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Service -@RequiredArgsConstructor -@Slf4j -@Transactional(readOnly = true) -public class CloudResourceWorkerService { - - private final CloudResourceWorkerMapRepository cloudResourceWorkerMapRepository; - private final WorkerRepository workerRepository; - private final CloudResourceRepository cloudResourceRepository; - - /** - * 클라우드 리소스에 Worker 할당 - * - * @param resourceId 클라우드 리소스 ID - * @param workerId Worker ID - * @return 생성된 CloudResourceWorkerMap - * @throws BusinessException 리소스, Worker를 찾을 수 없거나 이미 할당된 경우 - */ - @Transactional - public CloudResourceWorkerMap assignWorkerToResource(Long resourceId, Long workerId) { - log.info("[CloudResourceWorkerService] assignWorkerToResource - resourceId={}, workerId={}", - resourceId, workerId); - - // 클라우드 리소스 존재 확인 - CloudResource resource = cloudResourceRepository.findById(resourceId) - .orElseThrow(() -> new BusinessException( - com.agenticcp.core.domain.cloud.exception.CloudErrorCode.CLOUD_RESOURCE_NOT_FOUND)); - - // Worker 존재 확인 - Worker worker = workerRepository.findById(workerId) - .orElseThrow(() -> new BusinessException(WorkerErrorCode.WORKER_NOT_FOUND)); - - // 이미 할당되어 있는지 확인 - if (cloudResourceWorkerMapRepository.existsByResourceIdAndWorkerId(resourceId, workerId)) { - throw new BusinessException(WorkerErrorCode.CLOUD_RESOURCE_WORKER_MAP_ALREADY_EXISTS, - "이미 할당된 Worker입니다."); - } - - // CloudResourceWorkerMap 생성 - CloudResourceWorkerMap map = CloudResourceWorkerMap.builder() - .cloudResource(resource) - .worker(worker) - .isDeleted(false) - .build(); - - CloudResourceWorkerMap savedMap = cloudResourceWorkerMapRepository.save(map); - - log.info("[CloudResourceWorkerService] assignWorkerToResource - success resourceId={}, workerId={}", - resourceId, workerId); - - return savedMap; - } - - /** - * 클라우드 리소스에서 Worker 제거 - * - * @param resourceId 클라우드 리소스 ID - * @param workerId Worker ID - * @throws BusinessException CloudResourceWorkerMap을 찾을 수 없는 경우 - */ - @Transactional - public void removeWorkerFromResource(Long resourceId, Long workerId) { - log.info("[CloudResourceWorkerService] removeWorkerFromResource - resourceId={}, workerId={}", - resourceId, workerId); - - CloudResourceWorkerMap map = cloudResourceWorkerMapRepository - .findByResourceIdAndWorkerId(resourceId, workerId) - .orElseThrow(() -> new BusinessException(WorkerErrorCode.CLOUD_RESOURCE_WORKER_MAP_NOT_FOUND)); - - cloudResourceWorkerMapRepository.deleteByResourceIdAndWorkerId(resourceId, workerId); - - log.info("[CloudResourceWorkerService] removeWorkerFromResource - success resourceId={}, workerId={}", - resourceId, workerId); - } - - /** - * 클라우드 리소스의 Worker 목록 조회 - * - * @param resourceId 클라우드 리소스 ID - * @return CloudResourceWorkerMap 목록 - */ - public List findByResourceId(Long resourceId) { - log.info("[CloudResourceWorkerService] findByResourceId - resourceId={}", resourceId); - return cloudResourceWorkerMapRepository.findByResourceId(resourceId); - } - - /** - * Worker가 접근 가능한 리소스 목록 조회 - * - * @param workerId Worker ID - * @return CloudResourceWorkerMap 목록 - */ - public List findByWorkerId(Long workerId) { - log.info("[CloudResourceWorkerService] findByWorkerId - workerId={}", workerId); - return cloudResourceWorkerMapRepository.findByWorkerId(workerId); - } - - /** - * Worker가 특정 리소스에 접근 가능한지 확인 - * - * @param workerId Worker ID - * @param resourceId 클라우드 리소스 ID - * @return 접근 가능 여부 - */ - public boolean hasAccessToResource(Long workerId, Long resourceId) { - log.info("[CloudResourceWorkerService] hasAccessToResource - workerId={}, resourceId={}", - workerId, resourceId); - - boolean hasAccess = cloudResourceWorkerMapRepository.existsByResourceIdAndWorkerId(resourceId, workerId); - - log.info("[CloudResourceWorkerService] hasAccessToResource - result={}, workerId={}, resourceId={}", - hasAccess, workerId, resourceId); - - return hasAccess; - } - - /** - * 테넌트의 모든 리소스에 Worker 할당 - * (테넌트의 모든 리소스에 접근 권한 부여) - * - * @param tenantId 테넌트 ID - * @param workerId Worker ID - * @return 생성된 CloudResourceWorkerMap 목록 - */ - @Transactional - public List assignWorkerToTenantResources(Long tenantId, Long workerId) { - log.info("[CloudResourceWorkerService] assignWorkerToTenantResources - tenantId={}, workerId={}", - tenantId, workerId); - - // Worker 존재 확인 - Worker worker = workerRepository.findById(workerId) - .orElseThrow(() -> new BusinessException(WorkerErrorCode.WORKER_NOT_FOUND)); - - // 테넌트의 모든 리소스 조회 - List resources = cloudResourceRepository.findAll().stream() - .filter(resource -> resource.getTenant() != null && resource.getTenant().getId().equals(tenantId)) - .filter(resource -> !resource.getIsDeleted()) - .toList(); - - // 각 리소스에 Worker 할당 - return resources.stream() - .filter(resource -> !cloudResourceWorkerMapRepository.existsByResourceIdAndWorkerId(resource.getId(), workerId)) - .map(resource -> { - CloudResourceWorkerMap map = CloudResourceWorkerMap.builder() - .cloudResource(resource) - .worker(worker) - .isDeleted(false) - .build(); - return cloudResourceWorkerMapRepository.save(map); - }) - .toList(); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/monitoring/service/HealthCheckService.java b/src/main/java/com/agenticcp/core/domain/monitoring/service/HealthCheckService.java index e7e89bede..6985e8a97 100644 --- a/src/main/java/com/agenticcp/core/domain/monitoring/service/HealthCheckService.java +++ b/src/main/java/com/agenticcp/core/domain/monitoring/service/HealthCheckService.java @@ -175,10 +175,8 @@ private void sendSystemLevelAlert(String serviceName, String previousStatus, Str } // 첫 번째 시스템 관리자의 테넌트 ID 사용 - // 설계 B: User는 전역 계정이므로 tenant 필드 제거됨 - // TODO: Worker를 통해 테넌트 정보 가져오기 User systemAdmin = systemAdmins.get(0); - String tenantId = null; // 임시로 null 반환 (Worker를 통해 가져와야 함) + String tenantId = systemAdmin.getTenant().getTenantKey(); log.info("[HealthCheckService] sendSystemLevelAlert - 시스템 장애 이벤트 발행: serviceName={}, status={}->{}, admin={}", serviceName, previousStatus, currentStatus, systemAdmin.getName()); diff --git a/src/main/java/com/agenticcp/core/domain/notification/service/MonitoringNotificationService.java b/src/main/java/com/agenticcp/core/domain/notification/service/MonitoringNotificationService.java index ac2d0a76a..eda0e135c 100644 --- a/src/main/java/com/agenticcp/core/domain/notification/service/MonitoringNotificationService.java +++ b/src/main/java/com/agenticcp/core/domain/notification/service/MonitoringNotificationService.java @@ -481,10 +481,8 @@ private Long getTenantAdminUserId(String tenantId) { Tenant tenant = tenantOpt.get(); // 2. 테넌트의 관리자 조회 (TENANT_ADMIN 역할) - // 설계 B: User는 전역 계정이므로 tenant 필드 제거됨 - // Worker를 통해 테넌트별 사용자 조회 필요 - // TODO: Worker를 통해 테넌트별 활성 사용자 조회 구현 - Optional adminOpt = java.util.List.of() // 임시로 빈 리스트 반환 + Optional adminOpt = userRepository + .findActiveUsersByTenant(tenant, Status.ACTIVE) .stream() .filter(user -> user.getRole() == UserRole.TENANT_ADMIN) .findFirst(); diff --git a/src/main/java/com/agenticcp/core/domain/organization/controller/OrganizationController.java b/src/main/java/com/agenticcp/core/domain/organization/controller/OrganizationController.java index ad949e197..99fbb5230 100644 --- a/src/main/java/com/agenticcp/core/domain/organization/controller/OrganizationController.java +++ b/src/main/java/com/agenticcp/core/domain/organization/controller/OrganizationController.java @@ -5,9 +5,7 @@ import com.agenticcp.core.domain.organization.dto.OrganizationResponse; import com.agenticcp.core.domain.organization.dto.UpdateOrganizationRequest; import com.agenticcp.core.domain.organization.dto.OrganizationStatsResponse; -import com.agenticcp.core.domain.organization.dto.WorkerResponse; import com.agenticcp.core.domain.organization.service.OrganizationService; -import com.agenticcp.core.domain.organization.service.WorkerService; import com.agenticcp.core.domain.tenant.entity.Tenant; // [DEPRECATED imports - 주석처리된 API에서 사용] // import com.agenticcp.core.domain.organization.dto.AddUserToOrganizationRequest; @@ -49,7 +47,6 @@ public class OrganizationController { private final OrganizationService organizationService; - private final WorkerService workerService; /** * 조직 생성 @@ -295,10 +292,10 @@ public ResponseEntity> getOrganizationSta } // ========== [DEPRECATED] 조직-사용자 관계 API ========== - // ✅ 완료: #172에 따라 OrganizationMember API로 대체 완료 - // - OrganizationMemberController: /api/v1/organizations/{organizationId}/members - // - UserOrganizationController: /api/v1/users/{userId}/organizations - // - 아래 API들은 주석 처리되어 있으며, OrganizationMember API 사용 권장 + // TODO: #163 ERD에 따라 OrganizationMember를 통해 관리되도록 변경 예정 + // - User → Worker 엔티티로 변경 + // - OrganizationMember 테이블을 통한 관계 관리 + // - #163 구현 완료 후 아래 API들 제거 예정 /* @GetMapping("/{id}/users") @@ -421,34 +418,4 @@ public ResponseEntity>> getOrganizationTenantInf return ResponseEntity.ok(ApiResponse.success(info, "조직 테넌트 정보를 성공적으로 조회했습니다.")); } - - /** - * Worker 생성 (Organization 기반) - * - * @param id 조직 ID - * @return 생성된 Worker 정보 - */ - @PostMapping("/{id}/workers") - @Operation( - summary = "Worker 생성 (Organization 기반)", - description = "Organization 기반으로 Worker를 생성합니다. 설계 C 기준: Worker는 테넌트 독립적입니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "Worker 생성 성공", - content = @Content(schema = @Schema(implementation = WorkerResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "조직을 찾을 수 없음"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 존재하는 Worker") - }) - public ResponseEntity> createWorkerFromOrganization( - @Parameter(description = "조직 ID", required = true, example = "1") - @PathVariable @Positive Long id) { - log.info("[OrganizationController] createWorkerFromOrganization - organizationId={}", id); - - WorkerResponse response = WorkerResponse.from( - workerService.createWorkerFromOrganization(id)); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(response, "Worker가 성공적으로 생성되었습니다.")); - } } \ No newline at end of file diff --git a/src/main/java/com/agenticcp/core/domain/organization/controller/OrganizationMemberController.java b/src/main/java/com/agenticcp/core/domain/organization/controller/OrganizationMemberController.java deleted file mode 100644 index 0fdc6f893..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/controller/OrganizationMemberController.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.agenticcp.core.domain.organization.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.organization.dto.AddMemberRequest; -import com.agenticcp.core.domain.organization.dto.OrganizationMemberResponse; -import com.agenticcp.core.domain.organization.service.OrganizationMemberService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * 조직 멤버 관리 컨트롤러 - * - *

조직과 사용자 간의 관계를 관리하는 API를 제공합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/organizations/{organizationId}/members") -@RequiredArgsConstructor -@Tag(name = "Organization Member Management", description = "조직 멤버 관리 API") -public class OrganizationMemberController { - - private final OrganizationMemberService organizationMemberService; - - /** - * 조직의 멤버 목록 조회 - * - * @param organizationId 조직 ID - * @return 멤버 목록 - */ - @GetMapping - @Operation( - summary = "조직 멤버 목록 조회", - description = "특정 조직의 멤버 목록을 조회합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공", - content = @Content(schema = @Schema(implementation = OrganizationMemberResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "조직을 찾을 수 없음") - }) - public ResponseEntity>> getMembers( - @Parameter(description = "조직 ID", required = true, example = "1") - @PathVariable @Positive Long organizationId) { - log.info("[OrganizationMemberController] getMembers - organizationId={}", organizationId); - - List responses = organizationMemberService.getMembers(organizationId) - .stream() - .map(OrganizationMemberResponse::from) - .collect(Collectors.toList()); - - return ResponseEntity.ok(ApiResponse.success(responses, "멤버 목록을 성공적으로 조회했습니다.")); - } - - /** - * 조직에 멤버 추가 - * - * @param organizationId 조직 ID - * @param request 멤버 추가 요청 정보 - * @return 추가된 멤버 정보 - */ - @PostMapping - @Operation( - summary = "조직에 멤버 추가", - description = "조직에 사용자를 멤버로 추가합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "멤버 추가 성공", - content = @Content(schema = @Schema(implementation = OrganizationMemberResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "조직 또는 사용자를 찾을 수 없음"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 멤버로 등록된 사용자") - }) - public ResponseEntity> addMember( - @Parameter(description = "조직 ID", required = true, example = "1") - @PathVariable @Positive Long organizationId, - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "멤버 추가 요청 정보", - required = true, - content = @Content(schema = @Schema(implementation = AddMemberRequest.class)) - ) - @Valid @RequestBody AddMemberRequest request) { - log.info("[OrganizationMemberController] addMember - organizationId={}, userId={}", - organizationId, request.getUserId()); - - OrganizationMemberResponse response = OrganizationMemberResponse.from( - organizationMemberService.addMember(organizationId, request.getUserId(), request.getRole())); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(response, "멤버가 성공적으로 추가되었습니다.")); - } - - /** - * 조직에서 멤버 제거 - * - * @param organizationId 조직 ID - * @param userId 사용자 ID - */ - @DeleteMapping("/{userId}") - @Operation( - summary = "조직에서 멤버 제거", - description = "조직에서 사용자를 멤버에서 제거합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "204", description = "멤버 제거 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "조직 또는 멤버를 찾을 수 없음") - }) - public ResponseEntity removeMember( - @Parameter(description = "조직 ID", required = true, example = "1") - @PathVariable @Positive Long organizationId, - @Parameter(description = "사용자 ID", required = true, example = "1") - @PathVariable @Positive Long userId) { - log.info("[OrganizationMemberController] removeMember - organizationId={}, userId={}", - organizationId, userId); - - organizationMemberService.removeMember(organizationId, userId); - - return ResponseEntity.noContent().build(); - } - -} diff --git a/src/main/java/com/agenticcp/core/domain/organization/controller/TenantWorkerController.java b/src/main/java/com/agenticcp/core/domain/organization/controller/TenantWorkerController.java deleted file mode 100644 index b8ccd3a51..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/controller/TenantWorkerController.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.agenticcp.core.domain.organization.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.organization.dto.AssignWorkerRequest; -import com.agenticcp.core.domain.organization.dto.TenantWorkerMapResponse; -import com.agenticcp.core.domain.organization.service.TenantWorkerService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * 테넌트-Worker 관리 컨트롤러 - * - *

테넌트와 Worker 간의 관계를 관리하는 API입니다. - * Shared Tenant에 Worker를 할당하고 관리합니다.

- * - * @deprecated 설계 C 기준: TenantWorkerMap은 제거되었으며, CloudResourceWorkerController를 사용합니다. - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Deprecated -@Slf4j -@RestController -@RequestMapping("/api/v1/tenants/{tenantId}/workers") -@RequiredArgsConstructor -@Tag(name = "Tenant Worker Management", description = "테넌트-Worker 관리 API") -public class TenantWorkerController { - - private final TenantWorkerService tenantWorkerService; - - /** - * 테넌트의 Worker 목록 조회 - * - * @param tenantId 테넌트 ID - * @return TenantWorkerMap 목록 - */ - @GetMapping - @Operation( - summary = "테넌트의 Worker 목록 조회", - description = "특정 테넌트에 할당된 Worker 목록을 조회합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공", - content = @Content(schema = @Schema(implementation = TenantWorkerMapResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "테넌트를 찾을 수 없음") - }) - public ResponseEntity>> getWorkers( - @Parameter(description = "테넌트 ID", required = true, example = "1") - @PathVariable @Positive Long tenantId) { - log.info("[TenantWorkerController] getWorkers - tenantId={}", tenantId); - - List responses = tenantWorkerService.findByTenantId(tenantId) - .stream() - .map(TenantWorkerMapResponse::from) - .collect(Collectors.toList()); - - return ResponseEntity.ok(ApiResponse.success(responses, "Worker 목록을 성공적으로 조회했습니다.")); - } - - /** - * 테넌트에 Worker 할당 (Shared Tenant용) - * - * @param tenantId 테넌트 ID - * @param request Worker 할당 요청 정보 - * @return 할당된 TenantWorkerMap 정보 - */ - @PostMapping - @Operation( - summary = "테넌트에 Worker 할당", - description = "Shared Tenant에 Worker를 할당합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "Worker 할당 성공", - content = @Content(schema = @Schema(implementation = TenantWorkerMapResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "테넌트 또는 Worker를 찾을 수 없음"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 할당된 Worker") - }) - public ResponseEntity> assignWorker( - @Parameter(description = "테넌트 ID", required = true, example = "1") - @PathVariable @Positive Long tenantId, - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "Worker 할당 요청 정보", - required = true, - content = @Content(schema = @Schema(implementation = AssignWorkerRequest.class)) - ) - @Valid @RequestBody AssignWorkerRequest request) { - log.info("[TenantWorkerController] assignWorker - tenantId={}, workerId={}", - tenantId, request.getWorkerId()); - - TenantWorkerMapResponse response = TenantWorkerMapResponse.from( - tenantWorkerService.assignWorkerToTenant(tenantId, request.getWorkerId(), request.getAccessScope())); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(response, "Worker가 성공적으로 할당되었습니다.")); - } - - /** - * 테넌트에서 Worker 제거 - * - * @param tenantId 테넌트 ID - * @param workerId Worker ID - */ - @DeleteMapping("/{workerId}") - @Operation( - summary = "테넌트에서 Worker 제거", - description = "테넌트에서 Worker를 제거합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "204", description = "Worker 제거 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "테넌트 또는 Worker를 찾을 수 없음") - }) - public ResponseEntity removeWorker( - @Parameter(description = "테넌트 ID", required = true, example = "1") - @PathVariable @Positive Long tenantId, - @Parameter(description = "Worker ID", required = true, example = "1") - @PathVariable @Positive Long workerId) { - log.info("[TenantWorkerController] removeWorker - tenantId={}, workerId={}", tenantId, workerId); - - tenantWorkerService.removeWorkerFromTenant(tenantId, workerId); - - return ResponseEntity.noContent().build(); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/controller/UserOrganizationController.java b/src/main/java/com/agenticcp/core/domain/organization/controller/UserOrganizationController.java deleted file mode 100644 index b9eaf22b3..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/controller/UserOrganizationController.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.agenticcp.core.domain.organization.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.organization.dto.OrganizationMemberResponse; -import com.agenticcp.core.domain.organization.service.OrganizationMemberService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.Positive; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * 사용자 조직 관리 컨트롤러 - * - *

사용자가 속한 조직 목록을 조회하는 API를 제공합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/users/{userId}/organizations") -@RequiredArgsConstructor -@Tag(name = "User Organization Management", description = "사용자 조직 관리 API") -public class UserOrganizationController { - - private final OrganizationMemberService organizationMemberService; - - /** - * 사용자가 속한 조직 목록 조회 - * - * @param userId 사용자 ID - * @return 조직 멤버십 목록 - */ - @GetMapping - @Operation( - summary = "사용자의 조직 목록 조회", - description = "특정 사용자가 속한 조직 목록을 조회합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공", - content = @Content(schema = @Schema(implementation = OrganizationMemberResponse.class))) - }) - public ResponseEntity>> getOrganizationsByUserId( - @Parameter(description = "사용자 ID", required = true, example = "1") - @PathVariable @Positive Long userId) { - log.info("[UserOrganizationController] getOrganizationsByUserId - userId={}", userId); - - List responses = organizationMemberService.getOrganizationsByUserId(userId) - .stream() - .map(OrganizationMemberResponse::from) - .collect(Collectors.toList()); - - return ResponseEntity.ok(ApiResponse.success(responses, "조직 목록을 성공적으로 조회했습니다.")); - } -} diff --git a/src/main/java/com/agenticcp/core/domain/organization/controller/WorkerController.java b/src/main/java/com/agenticcp/core/domain/organization/controller/WorkerController.java deleted file mode 100644 index 9729829ad..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/controller/WorkerController.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.agenticcp.core.domain.organization.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.organization.dto.CreateWorkerRequest; -import com.agenticcp.core.domain.organization.dto.WorkerResponse; -import com.agenticcp.core.domain.organization.service.WorkerService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * Worker 관리 컨트롤러 - * - *

Worker의 생성 및 조회를 제공하는 API입니다. - * 설계 C 기준: Worker는 User 또는 Organization 기반으로 생성되며, 테넌트 독립적입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/users/{userId}/workers") -@RequiredArgsConstructor -@Tag(name = "Worker Management", description = "Worker 관리 API") -public class WorkerController { - - private final WorkerService workerService; - - /** - * Worker 생성 (User 기반) - * - * @param userId 사용자 ID - * @return 생성된 Worker 정보 - */ - @PostMapping - @Operation( - summary = "Worker 생성 (User 기반)", - description = "User 기반으로 Worker를 생성합니다. 설계 C 기준: Worker는 테넌트 독립적입니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "Worker 생성 성공", - content = @Content(schema = @Schema(implementation = WorkerResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 존재하는 Worker") - }) - public ResponseEntity> createWorkerFromUser( - @Parameter(description = "사용자 ID", required = true, example = "1") - @PathVariable @Positive Long userId) { - log.info("[WorkerController] createWorkerFromUser - userId={}", userId); - - WorkerResponse response = WorkerResponse.from( - workerService.createWorkerFromUser(userId)); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(response, "Worker가 성공적으로 생성되었습니다.")); - } - - /** - * 사용자의 Worker 목록 조회 - * - * @param userId 사용자 ID - * @return Worker 목록 - */ - @GetMapping - @Operation( - summary = "사용자의 Worker 목록 조회", - description = "특정 사용자의 Worker 목록을 조회합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공", - content = @Content(schema = @Schema(implementation = WorkerResponse.class))) - }) - public ResponseEntity>> getWorkersByUserId( - @Parameter(description = "사용자 ID", required = true, example = "1") - @PathVariable @Positive Long userId) { - log.info("[WorkerController] getWorkersByUserId - userId={}", userId); - - List responses = workerService.findByUserId(userId) - .stream() - .map(WorkerResponse::from) - .collect(Collectors.toList()); - - return ResponseEntity.ok(ApiResponse.success(responses, "Worker 목록을 성공적으로 조회했습니다.")); - } - - /** - * Worker 조회 - * - * @param userId 사용자 ID - * @param workerId Worker ID - * @return Worker 정보 - */ - @GetMapping("/{workerId}") - @Operation( - summary = "Worker 조회", - description = "특정 Worker의 정보를 조회합니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공", - content = @Content(schema = @Schema(implementation = WorkerResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Worker를 찾을 수 없음") - }) - public ResponseEntity> getWorker( - @Parameter(description = "사용자 ID", required = true, example = "1") - @PathVariable @Positive Long userId, - @Parameter(description = "Worker ID", required = true, example = "1") - @PathVariable @Positive Long workerId) { - log.info("[WorkerController] getWorker - userId={}, workerId={}", userId, workerId); - - WorkerResponse response = WorkerResponse.from(workerService.findById(workerId)); - - return ResponseEntity.ok(ApiResponse.success(response, "Worker 정보를 성공적으로 조회했습니다.")); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/controller/WorkerRoleController.java b/src/main/java/com/agenticcp/core/domain/organization/controller/WorkerRoleController.java deleted file mode 100644 index e2da8879d..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/controller/WorkerRoleController.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.agenticcp.core.domain.organization.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.organization.dto.AssignRoleRequest; -import com.agenticcp.core.domain.organization.dto.WorkerRoleResponse; -import com.agenticcp.core.domain.organization.service.WorkerRoleService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * Worker 역할 관리 컨트롤러 - * - *

Worker에게 역할을 부여하고 관리하는 API입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Slf4j -@RestController -@RequestMapping("/api/v1/workers/{workerId}/roles") -@RequiredArgsConstructor -@Tag(name = "Worker Role Management", description = "Worker 역할 관리 API") -public class WorkerRoleController { - - private final WorkerRoleService workerRoleService; - - /** - * Worker의 역할 목록 조회 - * - * @param workerId Worker ID - * @return WorkerRole 목록 - */ - @GetMapping - @Operation( - summary = "Worker의 역할 목록 조회", - description = "특정 Worker의 역할 목록을 조회합니다. 설계 C 기준: Role이 이미 tenant_id를 가지므로 WorkerRole에는 tenant_id가 없습니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공", - content = @Content(schema = @Schema(implementation = WorkerRoleResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Worker를 찾을 수 없음") - }) - public ResponseEntity>> getRoles( - @Parameter(description = "Worker ID", required = true, example = "1") - @PathVariable @Positive Long workerId) { - log.info("[WorkerRoleController] getRoles - workerId={}", workerId); - - List responses = workerRoleService.findByWorkerId(workerId) - .stream() - .map(WorkerRoleResponse::from) - .collect(Collectors.toList()); - - return ResponseEntity.ok(ApiResponse.success(responses, "역할 목록을 성공적으로 조회했습니다.")); - } - - /** - * Worker에게 역할 부여 - * - * @param workerId Worker ID - * @param request 역할 부여 요청 정보 - * @return 부여된 WorkerRole 정보 - */ - @PutMapping - @Operation( - summary = "Worker에게 역할 부여", - description = "Worker에게 역할을 부여합니다. 설계 C 기준: Role이 이미 tenant_id를 가지므로 별도로 tenant_id를 지정할 필요가 없습니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "역할 부여 성공", - content = @Content(schema = @Schema(implementation = WorkerRoleResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청 데이터"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Worker 또는 Role을 찾을 수 없음"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 부여된 역할") - }) - public ResponseEntity> assignRole( - @Parameter(description = "Worker ID", required = true, example = "1") - @PathVariable @Positive Long workerId, - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "역할 부여 요청 정보", - required = true, - content = @Content(schema = @Schema(implementation = AssignRoleRequest.class)) - ) - @Valid @RequestBody AssignRoleRequest request) { - log.info("[WorkerRoleController] assignRole - workerId={}, roleId={}", - workerId, request.getRoleId()); - - WorkerRoleResponse response = WorkerRoleResponse.from( - workerRoleService.assignRole(workerId, request.getRoleId())); - - return ResponseEntity.ok(ApiResponse.success(response, "역할이 성공적으로 부여되었습니다.")); - } - - /** - * Worker에서 역할 제거 - * - * @param workerId Worker ID - * @param roleId Role ID - */ - @DeleteMapping - @Operation( - summary = "Worker에서 역할 제거", - description = "Worker에서 역할을 제거합니다. 설계 C 기준: Role이 이미 tenant_id를 가지므로 별도로 tenant_id를 지정할 필요가 없습니다." - ) - @io.swagger.v3.oas.annotations.responses.ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "204", description = "역할 제거 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "WorkerRole을 찾을 수 없음") - }) - public ResponseEntity removeRole( - @Parameter(description = "Worker ID", required = true, example = "1") - @PathVariable @Positive Long workerId, - @Parameter(description = "Role ID", required = true, example = "1") - @RequestParam @Positive Long roleId) { - log.info("[WorkerRoleController] removeRole - workerId={}, roleId={}", - workerId, roleId); - - workerRoleService.removeRole(workerId, roleId); - - return ResponseEntity.noContent().build(); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/dto/AddMemberRequest.java b/src/main/java/com/agenticcp/core/domain/organization/dto/AddMemberRequest.java deleted file mode 100644 index ccf6be62d..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/dto/AddMemberRequest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.agenticcp.core.domain.organization.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -/** - * 조직 멤버 추가 요청 DTO - * - *

조직에 사용자를 멤버로 추가하기 위한 요청 DTO입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -@Schema(description = "조직 멤버 추가 요청") -public class AddMemberRequest { - - /** 사용자 ID */ - @NotNull(message = "사용자 ID는 필수입니다") - @Positive(message = "사용자 ID는 양수여야 합니다") - @Schema(description = "사용자 ID", example = "1", required = true) - private Long userId; - - /** 조직 내 역할 (선택적) */ - @Size(max = 50, message = "역할은 50자를 초과할 수 없습니다") - @Schema(description = "조직 내 역할", example = "ADMIN") - private String role; -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/dto/AssignRoleRequest.java b/src/main/java/com/agenticcp/core/domain/organization/dto/AssignRoleRequest.java deleted file mode 100644 index 1d4853f30..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/dto/AssignRoleRequest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.agenticcp.core.domain.organization.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -/** - * Worker 역할 부여 요청 DTO - * - *

Worker에게 역할을 부여하기 위한 요청 DTO입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -@Schema(description = "Worker 역할 부여 요청") -public class AssignRoleRequest { - - /** 역할 ID (Role이 이미 tenant_id를 가짐) */ - @NotNull(message = "역할 ID는 필수입니다") - @Positive(message = "역할 ID는 양수여야 합니다") - @Schema(description = "역할 ID", example = "1", required = true) - private Long roleId; -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/dto/AssignWorkerRequest.java b/src/main/java/com/agenticcp/core/domain/organization/dto/AssignWorkerRequest.java deleted file mode 100644 index 6a2154781..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/dto/AssignWorkerRequest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.agenticcp.core.domain.organization.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -/** - * 테넌트에 Worker 할당 요청 DTO - * - *

Shared Tenant에 Worker를 할당하기 위한 요청 DTO입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -@Schema(description = "테넌트에 Worker 할당 요청") -public class AssignWorkerRequest { - - /** Worker ID */ - @NotNull(message = "Worker ID는 필수입니다") - @Positive(message = "Worker ID는 양수여야 합니다") - @Schema(description = "Worker ID", example = "1", required = true) - private Long workerId; - - /** 접근 범위 (선택적) */ - @Size(max = 30, message = "접근 범위는 30자를 초과할 수 없습니다") - @Schema(description = "접근 범위", example = "FULL") - private String accessScope; -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/dto/CreateWorkerRequest.java b/src/main/java/com/agenticcp/core/domain/organization/dto/CreateWorkerRequest.java deleted file mode 100644 index a4207fa14..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/dto/CreateWorkerRequest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.agenticcp.core.domain.organization.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -/** - * Worker 생성 요청 DTO - * - *

User 기반으로 Worker를 생성하기 위한 요청 DTO입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -@Schema(description = "Worker 생성 요청") -public class CreateWorkerRequest { - - /** 테넌트 ID */ - @NotNull(message = "테넌트 ID는 필수입니다") - @Positive(message = "테넌트 ID는 양수여야 합니다") - @Schema(description = "테넌트 ID", example = "1", required = true) - private Long tenantId; -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/dto/OrganizationMemberResponse.java b/src/main/java/com/agenticcp/core/domain/organization/dto/OrganizationMemberResponse.java deleted file mode 100644 index 9e7521972..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/dto/OrganizationMemberResponse.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.agenticcp.core.domain.organization.dto; - -import com.agenticcp.core.domain.organization.entity.OrganizationMember; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * OrganizationMember 응답 DTO - * - *

조직 멤버 정보를 표현하는 응답 DTO입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -@Schema(description = "조직 멤버 응답") -public class OrganizationMemberResponse { - - /** 조직 ID */ - @Schema(description = "조직 ID", example = "1") - private Long organizationId; - - /** 조직명 */ - @Schema(description = "조직명", example = "개발팀") - private String organizationName; - - /** 사용자 ID */ - @Schema(description = "사용자 ID", example = "1") - private Long userId; - - /** 사용자명 */ - @Schema(description = "사용자명", example = "john_doe") - private String username; - - /** 사용자 이메일 */ - @Schema(description = "사용자 이메일", example = "john@example.com") - private String userEmail; - - /** 사용자 이름 */ - @Schema(description = "사용자 이름", example = "John Doe") - private String userName; - - /** 조직 내 역할 */ - @Schema(description = "조직 내 역할", example = "ADMIN") - private String role; - - /** 가입일시 */ - @Schema(description = "가입일시", example = "2024-01-01T00:00:00") - private LocalDateTime joinedAt; - - /** 생성일시 */ - @Schema(description = "생성일시", example = "2024-01-01T00:00:00") - private LocalDateTime createdAt; - - /** - * OrganizationMember 엔티티를 OrganizationMemberResponse로 변환 - * - * @param member OrganizationMember 엔티티 - * @return OrganizationMember 응답 DTO - */ - public static OrganizationMemberResponse from(OrganizationMember member) { - if (member == null) { - return null; - } - - return OrganizationMemberResponse.builder() - .organizationId(member.getOrganization() != null ? member.getOrganization().getId() : null) - .organizationName(member.getOrganization() != null ? member.getOrganization().getName() : null) - .userId(member.getUser() != null ? member.getUser().getId() : null) - .username(member.getUser() != null ? member.getUser().getUsername() : null) - .userEmail(member.getUser() != null ? member.getUser().getEmail() : null) - .userName(member.getUser() != null ? member.getUser().getName() : null) - .role(member.getRole()) - .joinedAt(member.getJoinedAt()) - .createdAt(member.getCreatedAt()) - .build(); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/dto/OrganizationResponse.java b/src/main/java/com/agenticcp/core/domain/organization/dto/OrganizationResponse.java index b7be0e7c8..2202c7c13 100644 --- a/src/main/java/com/agenticcp/core/domain/organization/dto/OrganizationResponse.java +++ b/src/main/java/com/agenticcp/core/domain/organization/dto/OrganizationResponse.java @@ -137,13 +137,33 @@ public class OrganizationResponse { public static OrganizationResponse from(Organization organization) { OrganizationResponseBuilder builder = OrganizationResponse.builder() .id(organization.getId()) - // ERD 기준 필드 (설계 B: name만 존재) - .name(organization.getName()) + // ERD 기준 필드 + .name(organization.getName() != null ? organization.getName() : organization.getOrgName()) + // [DEPRECATED] 호환성 유지 + .orgKey(organization.getOrgKey()) + .orgName(organization.getOrgName()) + .description(organization.getDescription()) + .parentOrgId(organization.getParentOrganization() != null ? + organization.getParentOrganization().getId() : null) + .status(organization.getStatus() != null ? organization.getStatus().name() : null) + .orgType(organization.getOrgType() != null ? organization.getOrgType().name() : null) + .contactEmail(organization.getContactEmail()) + .contactPhone(organization.getContactPhone()) + .address(organization.getAddress()) + .website(organization.getWebsite()) + .maxUsers(organization.getMaxUsers()) + .settings(organization.getSettings()) + .establishedDate(organization.getEstablishedDate()) .createdAt(organization.getCreatedAt()) .updatedAt(organization.getUpdatedAt()); - // 설계 B: Organization은 tenant 필드가 없음 - // 테넌트 정보는 별도로 조회 필요 + // 테넌트 정보 (1:1 관계) + if (organization.getTenant() != null) { + builder.tenantId(organization.getTenant().getId()) + .tenantKey(organization.getTenant().getTenantKey()) + .tenantType(organization.getTenant().getTenantType() != null ? + organization.getTenant().getTenantType().name() : null); + } return builder.build(); } diff --git a/src/main/java/com/agenticcp/core/domain/organization/dto/TenantWorkerMapResponse.java b/src/main/java/com/agenticcp/core/domain/organization/dto/TenantWorkerMapResponse.java deleted file mode 100644 index a80d364ad..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/dto/TenantWorkerMapResponse.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.agenticcp.core.domain.organization.dto; - -import com.agenticcp.core.domain.organization.entity.TenantWorkerMap; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * TenantWorkerMap 응답 DTO - * - *

테넌트-Worker 매핑 정보를 표현하는 응답 DTO입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -@Schema(description = "테넌트-Worker 매핑 응답") -public class TenantWorkerMapResponse { - - /** 테넌트 ID */ - @Schema(description = "테넌트 ID", example = "1") - private Long tenantId; - - /** 테넌트 키 */ - @Schema(description = "테넌트 키", example = "tenant-dev") - private String tenantKey; - - /** 테넌트명 */ - @Schema(description = "테넌트명", example = "개발 테넌트") - private String tenantName; - - /** Worker ID */ - @Schema(description = "Worker ID", example = "1") - private Long workerId; - - /** 사용자 ID */ - @Schema(description = "사용자 ID", example = "1") - private Long userId; - - /** 사용자명 */ - @Schema(description = "사용자명", example = "john_doe") - private String username; - - /** 사용자 이메일 */ - @Schema(description = "사용자 이메일", example = "john@example.com") - private String userEmail; - - /** 사용자 이름 */ - @Schema(description = "사용자 이름", example = "John Doe") - private String userName; - - /** 접근 범위 */ - @Schema(description = "접근 범위", example = "FULL") - private String accessScope; - - /** 가입일시 */ - @Schema(description = "가입일시", example = "2024-01-01T00:00:00") - private LocalDateTime joinedAt; - - /** 생성일시 */ - @Schema(description = "생성일시", example = "2024-01-01T00:00:00") - private LocalDateTime createdAt; - - /** - * TenantWorkerMap 엔티티를 TenantWorkerMapResponse로 변환 - * - * @param map TenantWorkerMap 엔티티 - * @return TenantWorkerMap 응답 DTO - */ - public static TenantWorkerMapResponse from(TenantWorkerMap map) { - if (map == null) { - return null; - } - - return TenantWorkerMapResponse.builder() - .tenantId(map.getTenant() != null ? map.getTenant().getId() : null) - .tenantKey(map.getTenant() != null ? map.getTenant().getTenantKey() : null) - .tenantName(map.getTenant() != null ? map.getTenant().getTenantName() : null) - .workerId(map.getWorker() != null ? map.getWorker().getId() : null) - .userId(map.getWorker() != null && map.getWorker().getUser() != null - ? map.getWorker().getUser().getId() : null) - .username(map.getWorker() != null && map.getWorker().getUser() != null - ? map.getWorker().getUser().getUsername() : null) - .userEmail(map.getWorker() != null && map.getWorker().getUser() != null - ? map.getWorker().getUser().getEmail() : null) - .userName(map.getWorker() != null && map.getWorker().getUser() != null - ? map.getWorker().getUser().getName() : null) - .accessScope(map.getAccessScope()) - .joinedAt(map.getJoinedAt()) - .createdAt(map.getCreatedAt()) - .build(); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/dto/WorkerResponse.java b/src/main/java/com/agenticcp/core/domain/organization/dto/WorkerResponse.java deleted file mode 100644 index 274089954..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/dto/WorkerResponse.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.agenticcp.core.domain.organization.dto; - -import com.agenticcp.core.domain.organization.entity.Worker; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * Worker 응답 DTO - * - *

Worker 정보를 표현하는 응답 DTO입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -@Schema(description = "Worker 응답") -public class WorkerResponse { - - /** Worker ID */ - @Schema(description = "Worker ID", example = "1") - private Long id; - - /** 사용자 ID */ - @Schema(description = "사용자 ID", example = "1") - private Long userId; - - /** 사용자명 */ - @Schema(description = "사용자명", example = "john_doe") - private String username; - - /** 사용자 이메일 */ - @Schema(description = "사용자 이메일", example = "john@example.com") - private String userEmail; - - /** 사용자 이름 */ - @Schema(description = "사용자 이름", example = "John Doe") - private String userName; - - /** 조직 ID (Organization 기반 Worker인 경우) */ - @Schema(description = "조직 ID", example = "1") - private Long organizationId; - - /** 조직명 (Organization 기반 Worker인 경우) */ - @Schema(description = "조직명", example = "개발팀") - private String organizationName; - - /** 생성일시 */ - @Schema(description = "생성일시", example = "2024-01-01T00:00:00") - private LocalDateTime createdAt; - - /** 수정일시 */ - @Schema(description = "수정일시", example = "2024-01-01T00:00:00") - private LocalDateTime updatedAt; - - /** - * Worker 엔티티를 WorkerResponse로 변환 - * - * @param worker Worker 엔티티 - * @return Worker 응답 DTO - */ - public static WorkerResponse from(Worker worker) { - if (worker == null) { - return null; - } - - return WorkerResponse.builder() - .id(worker.getId()) - .userId(worker.getUser() != null ? worker.getUser().getId() : null) - .username(worker.getUser() != null ? worker.getUser().getUsername() : null) - .userEmail(worker.getUser() != null ? worker.getUser().getEmail() : null) - .userName(worker.getUser() != null ? worker.getUser().getName() : null) - .organizationId(worker.getOrganization() != null ? worker.getOrganization().getId() : null) - .organizationName(worker.getOrganization() != null ? worker.getOrganization().getName() : null) - .createdAt(worker.getCreatedAt()) - .updatedAt(worker.getUpdatedAt()) - .build(); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/dto/WorkerRoleResponse.java b/src/main/java/com/agenticcp/core/domain/organization/dto/WorkerRoleResponse.java deleted file mode 100644 index 25779ed8b..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/dto/WorkerRoleResponse.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.agenticcp.core.domain.organization.dto; - -import com.agenticcp.core.domain.organization.entity.WorkerRole; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * WorkerRole 응답 DTO - * - *

Worker 역할 정보를 표현하는 응답 DTO입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -@Schema(description = "Worker 역할 응답") -public class WorkerRoleResponse { - - /** Worker ID */ - @Schema(description = "Worker ID", example = "1") - private Long workerId; - - /** 사용자 ID */ - @Schema(description = "사용자 ID", example = "1") - private Long userId; - - /** 사용자명 */ - @Schema(description = "사용자명", example = "john_doe") - private String username; - - /** 역할 ID */ - @Schema(description = "역할 ID", example = "1") - private Long roleId; - - /** 역할 키 */ - @Schema(description = "역할 키", example = "ADMIN") - private String roleKey; - - /** 역할명 */ - @Schema(description = "역할명", example = "관리자") - private String roleName; - - /** 테넌트 ID */ - @Schema(description = "테넌트 ID", example = "1") - private Long tenantId; - - /** 테넌트 키 */ - @Schema(description = "테넌트 키", example = "tenant-dev") - private String tenantKey; - - /** 테넌트명 */ - @Schema(description = "테넌트명", example = "개발 테넌트") - private String tenantName; - - /** 생성일시 */ - @Schema(description = "생성일시", example = "2024-01-01T00:00:00") - private LocalDateTime createdAt; - - /** 수정일시 */ - @Schema(description = "수정일시", example = "2024-01-01T00:00:00") - private LocalDateTime updatedAt; - - /** - * WorkerRole 엔티티를 WorkerRoleResponse로 변환 - * - * @param workerRole WorkerRole 엔티티 - * @return WorkerRole 응답 DTO - */ - public static WorkerRoleResponse from(WorkerRole workerRole) { - if (workerRole == null) { - return null; - } - - return WorkerRoleResponse.builder() - .workerId(workerRole.getWorker() != null ? workerRole.getWorker().getId() : null) - .userId(workerRole.getWorker() != null && workerRole.getWorker().getUser() != null - ? workerRole.getWorker().getUser().getId() : null) - .username(workerRole.getWorker() != null && workerRole.getWorker().getUser() != null - ? workerRole.getWorker().getUser().getUsername() : null) - .roleId(workerRole.getRole() != null ? workerRole.getRole().getId() : null) - .roleKey(workerRole.getRole() != null ? workerRole.getRole().getRoleKey() : null) - .roleName(workerRole.getRole() != null ? workerRole.getRole().getRoleName() : null) - .tenantId(workerRole.getRole() != null && workerRole.getRole().getTenant() != null - ? workerRole.getRole().getTenant().getId() : null) - .tenantKey(workerRole.getRole() != null && workerRole.getRole().getTenant() != null - ? workerRole.getRole().getTenant().getTenantKey() : null) - .tenantName(workerRole.getRole() != null && workerRole.getRole().getTenant() != null - ? workerRole.getRole().getTenant().getTenantName() : null) - .createdAt(workerRole.getCreatedAt()) - .updatedAt(workerRole.getUpdatedAt()) - .build(); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/entity/Organization.java b/src/main/java/com/agenticcp/core/domain/organization/entity/Organization.java index a9445454c..15171a0c9 100644 --- a/src/main/java/com/agenticcp/core/domain/organization/entity/Organization.java +++ b/src/main/java/com/agenticcp/core/domain/organization/entity/Organization.java @@ -1,6 +1,8 @@ package com.agenticcp.core.domain.organization.entity; import com.agenticcp.core.common.entity.BaseEntity; +import com.agenticcp.core.common.enums.Status; +import com.agenticcp.core.domain.tenant.entity.Tenant; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @@ -10,6 +12,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; /** * 조직 엔티티 @@ -23,7 +26,12 @@ */ @Entity @Table(name = "organizations", indexes = { - @Index(name = "idx_organizations_name", columnList = "name") + @Index(name = "idx_organizations_name", columnList = "name"), + // [DEPRECATED] 아래 인덱스들은 컬럼 제거 시 함께 제거 예정 + @Index(name = "idx_organizations_org_key", columnList = "org_key"), + @Index(name = "idx_organizations_parent", columnList = "parent_org_id"), + @Index(name = "idx_organizations_status", columnList = "status"), + @Index(name = "idx_organizations_type", columnList = "org_type") }) @Data @Builder @@ -38,7 +46,91 @@ public class Organization extends BaseEntity { @Column(name = "name", nullable = false, length = 255) private String name; - /** 테넌트 (1:1 관계) */ - @OneToOne(mappedBy = "organization", fetch = FetchType.LAZY) - private com.agenticcp.core.domain.tenant.entity.Tenant tenant; + /** 테넌트 (1:1 관계) - ERD: organization_id FK in TENANT */ + @OneToOne(mappedBy = "organization", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Tenant tenant; + + // ========== [DEPRECATED] ERD에 없는 필드들 ========== + // TODO: 마이그레이션 완료 후 제거 예정 + + /** @deprecated ERD에 없음 - 호환성을 위해 임시 유지 */ + @Deprecated + @Column(name = "org_key", unique = true, length = 100) + private String orgKey; + + /** @deprecated ERD에 없음 - 호환성을 위해 임시 유지 */ + @Deprecated + @Column(name = "org_name", length = 255) + private String orgName; + + /** @deprecated ERD에 없음 */ + @Deprecated + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + /** @deprecated ERD에 계층 구조 없음 */ + @Deprecated + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_org_id") + private Organization parentOrganization; + + /** @deprecated ERD에 없음 */ + @Deprecated + @Enumerated(EnumType.STRING) + @Column(name = "status", length = 20) + @Builder.Default + private Status status = Status.ACTIVE; + + /** @deprecated ERD에 없음 */ + @Deprecated + @Enumerated(EnumType.STRING) + @Column(name = "org_type", length = 20) + private OrganizationType orgType; + + /** @deprecated ERD에 없음 */ + @Deprecated + @Column(name = "contact_email", length = 255) + private String contactEmail; + + /** @deprecated ERD에 없음 */ + @Deprecated + @Column(name = "contact_phone", length = 50) + private String contactPhone; + + /** @deprecated ERD에 없음 */ + @Deprecated + @Column(name = "address", columnDefinition = "TEXT") + private String address; + + /** @deprecated ERD에 없음 */ + @Deprecated + @Column(name = "website", length = 255) + private String website; + + /** @deprecated ERD에 없음 */ + @Deprecated + @Column(name = "max_users") + private Integer maxUsers; + + /** @deprecated ERD에 없음 */ + @Deprecated + @Column(name = "settings", columnDefinition = "TEXT") + private String settings; + + /** @deprecated ERD에 없음 */ + @Deprecated + @Column(name = "established_date") + private LocalDateTime establishedDate; + + /** + * @deprecated ERD에 없음 + */ + @Deprecated + public enum OrganizationType { + COMPANY, + DEPARTMENT, + TEAM, + PROJECT, + DIVISION + } } diff --git a/src/main/java/com/agenticcp/core/domain/organization/entity/OrganizationMember.java b/src/main/java/com/agenticcp/core/domain/organization/entity/OrganizationMember.java deleted file mode 100644 index 68a6f6f66..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/entity/OrganizationMember.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.agenticcp.core.domain.organization.entity; - -import com.agenticcp.core.domain.user.entity.User; -import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -/** - * OrganizationMember 엔티티 - * - *

조직과 사용자 간의 관계를 관리하는 엔티티입니다. - * 설계 B 기준: (organization_id, user_id)가 복합 PK

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Entity -@Table(name = "organization_member", indexes = { - @Index(name = "idx_org_member_org", columnList = "organization_id"), - @Index(name = "idx_org_member_user", columnList = "user_id") -}) -@IdClass(OrganizationMemberId.class) -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EntityListeners(AuditingEntityListener.class) -public class OrganizationMember { - - /** - * 조직 (복합 PK의 일부) - */ - @Id - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "organization_id", nullable = false) - private Organization organization; - - /** - * 사용자 (복합 PK의 일부) - */ - @Id - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - /** - * 조직 내 역할 (선택적) - */ - @Column(name = "role", length = 50) - private String role; - - /** - * 가입일시 - */ - @Column(name = "joined_at") - @Builder.Default - private LocalDateTime joinedAt = LocalDateTime.now(); - - /** - * 생성일시 - */ - @CreatedDate - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - /** - * 수정일시 - */ - @LastModifiedDate - @Column(name = "updated_at") - private LocalDateTime updatedAt; -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/entity/OrganizationMemberId.java b/src/main/java/com/agenticcp/core/domain/organization/entity/OrganizationMemberId.java deleted file mode 100644 index 65778bfe5..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/entity/OrganizationMemberId.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.agenticcp.core.domain.organization.entity; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; -import java.util.Objects; - -/** - * OrganizationMember 복합 PK 클래스 - * - *

설계 B 기준: (organization_id, user_id)가 복합 PK

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class OrganizationMemberId implements Serializable { - - // JPA @IdClass 사용 시 엔티티의 필드명과 일치해야 함 (organization, user) - private Long organization; - private Long user; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OrganizationMemberId that = (OrganizationMemberId) o; - return Objects.equals(organization, that.organization) && - Objects.equals(user, that.user); - } - - @Override - public int hashCode() { - return Objects.hash(organization, user); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/entity/TenantWorkerMap.java b/src/main/java/com/agenticcp/core/domain/organization/entity/TenantWorkerMap.java deleted file mode 100644 index 57bbc2a48..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/entity/TenantWorkerMap.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.agenticcp.core.domain.organization.entity; - -import com.agenticcp.core.domain.tenant.entity.Tenant; -import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -/** - * TenantWorkerMap 엔티티 - * - *

Worker가 어떤 테넌트에 소속되는지 정의하는 엔티티입니다. - * 설계 B 기준: 복합 PK (tenant_id, worker_id)를 사용합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Entity -@Table(name = "tenant_worker_map", uniqueConstraints = { - @UniqueConstraint(name = "uk_tenant_worker", columnNames = {"tenant_id", "worker_id"}) -}, indexes = { - @Index(name = "idx_tenant_worker_tenant", columnList = "tenant_id"), - @Index(name = "idx_tenant_worker_worker", columnList = "worker_id") -}) -@IdClass(TenantWorkerMapId.class) -@EntityListeners(AuditingEntityListener.class) -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -public class TenantWorkerMap { - - // 복합 PK의 일부 - 관계 필드에서 ID 추출 - @Id - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "tenant_id", nullable = false) - private Tenant tenant; - - @Id - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "worker_id", nullable = false) - private Worker worker; - - /** - * 공유 테넌트에서의 참여 범위 - */ - @Column(name = "access_scope", length = 30) - private String accessScope; - - /** - * 가입일시 - */ - @Column(name = "joined_at") - @Builder.Default - private LocalDateTime joinedAt = LocalDateTime.now(); - - /** - * 생성일시 - */ - @CreatedDate - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - /** - * 수정일시 - */ - @LastModifiedDate - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - /** - * 생성자 - */ - @Column(name = "created_by") - private String createdBy; - - /** - * 수정자 - */ - @Column(name = "updated_by") - private String updatedBy; - - /** - * 삭제 여부 - */ - @Column(name = "is_deleted", nullable = false) - @Builder.Default - private Boolean isDeleted = false; -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/entity/TenantWorkerMapId.java b/src/main/java/com/agenticcp/core/domain/organization/entity/TenantWorkerMapId.java deleted file mode 100644 index b0f151a38..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/entity/TenantWorkerMapId.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.agenticcp.core.domain.organization.entity; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; -import java.util.Objects; - -/** - * TenantWorkerMap 복합 PK 클래스 - * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class TenantWorkerMapId implements Serializable { - - // JPA @IdClass 사용 시 엔티티의 필드명과 일치해야 함 (tenant, worker) - private Long tenant; - private Long worker; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TenantWorkerMapId that = (TenantWorkerMapId) o; - return Objects.equals(tenant, that.tenant) && - Objects.equals(worker, that.worker); - } - - @Override - public int hashCode() { - return Objects.hash(tenant, worker); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/entity/Worker.java b/src/main/java/com/agenticcp/core/domain/organization/entity/Worker.java deleted file mode 100644 index 9c9598a3a..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/entity/Worker.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.agenticcp.core.domain.organization.entity; - -import com.agenticcp.core.common.entity.BaseEntity; -import com.agenticcp.core.domain.user.entity.User; -import jakarta.persistence.*; -import jakarta.validation.constraints.AssertTrue; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -/** - * Worker 엔티티 - * - *

User 또는 Organization을 Worker로 변환하는 엔티티입니다. - * 설계 C 기준: User와 Organization 모두 Worker로 변환 가능하며, - * user_id와 organization_id 중 하나만 NOT NULL이어야 합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Entity -@Table(name = "workers", indexes = { - @Index(name = "idx_worker_user", columnList = "user_id"), - @Index(name = "idx_worker_organization", columnList = "organization_id") -}) -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode(callSuper = false) -public class Worker extends BaseEntity { - - /** - * 전역 User (User 기반 Worker) - * user_id와 organization_id 중 하나만 NOT NULL이어야 함 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; - - /** - * 조직 (Organization 기반 Worker) - * user_id와 organization_id 중 하나만 NOT NULL이어야 함 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "organization_id") - private Organization organization; - - /** - * user_id와 organization_id 중 하나만 NOT NULL인지 검증 - */ - @AssertTrue(message = "user_id와 organization_id 중 하나만 설정되어야 합니다") - private boolean isValidWorkerType() { - return (user != null && organization == null) || - (user == null && organization != null); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/entity/WorkerRole.java b/src/main/java/com/agenticcp/core/domain/organization/entity/WorkerRole.java deleted file mode 100644 index 7dd255a03..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/entity/WorkerRole.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.agenticcp.core.domain.organization.entity; - -import com.agenticcp.core.domain.user.entity.Role; -import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -/** - * WorkerRole 엔티티 - * - *

Worker가 테넌트 내에서 수행할 역할을 정의하는 엔티티입니다. - * 설계 C 기준: 복합 PK (worker_id, role_id)를 사용합니다. - * tenant_id는 제거되었으며, Role이 이미 tenant_id를 가지므로 중복입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Entity -@Table(name = "worker_role", uniqueConstraints = { - @UniqueConstraint(name = "uk_worker_role", columnNames = {"worker_id", "role_id"}) -}, indexes = { - @Index(name = "idx_worker_role_worker", columnList = "worker_id"), - @Index(name = "idx_worker_role_role", columnList = "role_id") -}) -@IdClass(WorkerRoleId.class) -@EntityListeners(AuditingEntityListener.class) -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -public class WorkerRole { - - // 복합 PK의 일부 - 관계 필드에서 ID 추출 - @Id - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "worker_id", nullable = false) - private Worker worker; - - @Id - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "role_id", nullable = false) - private Role role; - - /** - * 생성일시 - */ - @CreatedDate - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - /** - * 수정일시 - */ - @LastModifiedDate - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - /** - * 생성자 - */ - @Column(name = "created_by") - private String createdBy; - - /** - * 수정자 - */ - @Column(name = "updated_by") - private String updatedBy; - - /** - * 삭제 여부 - */ - @Column(name = "is_deleted", nullable = false) - @Builder.Default - private Boolean isDeleted = false; -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/entity/WorkerRoleId.java b/src/main/java/com/agenticcp/core/domain/organization/entity/WorkerRoleId.java deleted file mode 100644 index c975a7e8a..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/entity/WorkerRoleId.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.agenticcp.core.domain.organization.entity; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; -import java.util.Objects; - -/** - * WorkerRole 복합 PK 클래스 - * - *

설계 C 기준: (worker_id, role_id) 복합 PK - * tenant_id는 제거되었습니다 (Role이 이미 tenant_id를 가짐)

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class WorkerRoleId implements Serializable { - - // JPA @IdClass 사용 시 엔티티의 필드명과 일치해야 함 (worker, role) - private Long worker; - private Long role; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - WorkerRoleId that = (WorkerRoleId) o; - return Objects.equals(worker, that.worker) && - Objects.equals(role, that.role); - } - - @Override - public int hashCode() { - return Objects.hash(worker, role); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/enums/WorkerErrorCode.java b/src/main/java/com/agenticcp/core/domain/organization/enums/WorkerErrorCode.java deleted file mode 100644 index 85870506d..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/enums/WorkerErrorCode.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.agenticcp.core.domain.organization.enums; - -import com.agenticcp.core.common.dto.exception.BaseErrorCode; -import com.agenticcp.core.common.enums.ErrorCategory; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -/** - * Worker 도메인에서 사용되는 에러 코드를 정의하는 Enum 클래스입니다. - * - * @see BaseErrorCode - * @see ErrorCategory - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Getter -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public enum WorkerErrorCode implements BaseErrorCode { - - // Worker 관련 (12001-12020) - WORKER_NOT_FOUND(HttpStatus.NOT_FOUND, 12001, "Worker를 찾을 수 없습니다."), - WORKER_DUPLICATE_USER(HttpStatus.CONFLICT, 12002, "해당 User로 이미 Worker가 생성되었습니다."), - WORKER_DUPLICATE_ORGANIZATION(HttpStatus.CONFLICT, 12003, "해당 Organization으로 이미 Worker가 생성되었습니다."), - WORKER_DUPLICATE_USER_TENANT(HttpStatus.CONFLICT, 12004, "[DEPRECATED] 같은 User와 Tenant로 이미 Worker가 생성되었습니다."), - - // Organization 관련 (Worker 도메인에서 사용) - ORGANIZATION_NOT_FOUND(HttpStatus.NOT_FOUND, 12005, "조직을 찾을 수 없습니다."), - - // Tenant 관련 (Worker 도메인에서 사용) - TENANT_NOT_FOUND(HttpStatus.NOT_FOUND, 12006, "테넌트를 찾을 수 없습니다."), - - // TenantWorkerMap 관련 (12021-12040) [DEPRECATED] - TENANT_WORKER_MAP_NOT_FOUND(HttpStatus.NOT_FOUND, 12021, "[DEPRECATED] TenantWorkerMap을 찾을 수 없습니다."), - TENANT_WORKER_MAP_ALREADY_EXISTS(HttpStatus.CONFLICT, 12022, "[DEPRECATED] 이미 할당된 Worker입니다."), - TENANT_WORKER_ACCESS_DENIED(HttpStatus.FORBIDDEN, 12023, "[DEPRECATED] 테넌트 접근 권한이 없습니다."), - - // CloudResourceWorkerMap 관련 (12031-12040) - CLOUD_RESOURCE_WORKER_MAP_NOT_FOUND(HttpStatus.NOT_FOUND, 12031, "CloudResourceWorkerMap을 찾을 수 없습니다."), - CLOUD_RESOURCE_WORKER_MAP_ALREADY_EXISTS(HttpStatus.CONFLICT, 12032, "이미 할당된 Worker입니다."), - CLOUD_RESOURCE_ACCESS_DENIED(HttpStatus.FORBIDDEN, 12033, "클라우드 리소스 접근 권한이 없습니다."), - - // WorkerRole 관련 (12041-12060) - WORKER_ROLE_NOT_FOUND(HttpStatus.NOT_FOUND, 12041, "WorkerRole을 찾을 수 없습니다."), - WORKER_ROLE_ALREADY_EXISTS(HttpStatus.CONFLICT, 12042, "이미 부여된 역할입니다."), - WORKER_ROLE_INVALID(HttpStatus.BAD_REQUEST, 12043, "유효하지 않은 역할입니다."); - - private final HttpStatus httpStatus; - private final int codeNumber; - private final String message; - - @Override - public String getCode() { - return ErrorCategory.WORKER.generate(codeNumber); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/repository/OrganizationMemberRepository.java b/src/main/java/com/agenticcp/core/domain/organization/repository/OrganizationMemberRepository.java deleted file mode 100644 index 31f9154c9..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/repository/OrganizationMemberRepository.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.agenticcp.core.domain.organization.repository; - -import com.agenticcp.core.domain.organization.entity.OrganizationMember; -import com.agenticcp.core.domain.organization.entity.OrganizationMemberId; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -/** - * OrganizationMember Repository - * - *

OrganizationMember 엔티티에 대한 데이터 접근을 제공합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Repository -public interface OrganizationMemberRepository extends JpaRepository { - - /** - * 조직 ID로 멤버 목록 조회 - * - * @param organizationId 조직 ID - * @return 멤버 목록 - */ - List findByOrganizationId(Long organizationId); - - /** - * 사용자 ID로 멤버 목록 조회 - * - * @param userId 사용자 ID - * @return 멤버 목록 - */ - List findByUserId(Long userId); - - /** - * 조직 ID와 사용자 ID로 멤버 조회 - * - * @param organizationId 조직 ID - * @param userId 사용자 ID - * @return 멤버 (Optional) - */ - Optional findByOrganizationIdAndUserId(Long organizationId, Long userId); - - /** - * 조직 ID와 사용자 ID로 멤버 존재 여부 확인 - * - * @param organizationId 조직 ID - * @param userId 사용자 ID - * @return 존재 여부 - */ - boolean existsByOrganizationIdAndUserId(Long organizationId, Long userId); - - /** - * 조직 ID와 사용자 ID로 멤버 삭제 - * - * @param organizationId 조직 ID - * @param userId 사용자 ID - */ - void deleteByOrganizationIdAndUserId(Long organizationId, Long userId); -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/repository/OrganizationRepository.java b/src/main/java/com/agenticcp/core/domain/organization/repository/OrganizationRepository.java index e95816fa2..8bd5d2095 100644 --- a/src/main/java/com/agenticcp/core/domain/organization/repository/OrganizationRepository.java +++ b/src/main/java/com/agenticcp/core/domain/organization/repository/OrganizationRepository.java @@ -23,58 +23,45 @@ public interface OrganizationRepository extends JpaRepository { /** - * 조직명 중복 검사 (설계 B: name 필드 사용) - * @param name 조직명 + * 조직명 중복 검사 + * @param orgName 조직명 * @return 중복 여부 */ - boolean existsByName(String name); + boolean existsByOrgName(String orgName); /** - * 조직명 중복 검사 (Deprecated - 호환성 유지) - * @deprecated existsByName 사용 권장 - */ - @Deprecated - @Query("SELECT COUNT(o) > 0 FROM Organization o WHERE o.name = :orgName") - boolean existsByOrgName(@Param("orgName") String orgName); - - /** - * 조직 키 중복 검사 (Deprecated - 설계 B에 없음) - * @deprecated 설계 B에는 orgKey 필드가 없음 + * 조직 키 중복 검사 + * @param orgKey 조직 키 + * @return 중복 여부 */ - @Deprecated - @Query("SELECT false FROM Organization o WHERE 1=0") boolean existsByOrgKey(String orgKey); /** - * 하위 조직 존재 여부 확인 (Deprecated - 설계 B에 계층 구조 없음) - * @deprecated 설계 B에는 계층 구조가 없음 + * 하위 조직 존재 여부 확인 + * @param parentOrgId 상위 조직 ID + * @return 하위 조직 존재 여부 */ - @Deprecated - @Query("SELECT false FROM Organization o WHERE 1=0") boolean existsByParentOrganizationId(Long parentOrgId); /** - * 활성 조직 목록 조회 (Deprecated - 설계 B에 status 필드 없음) - * @deprecated 설계 B에는 status 필드가 없음 + * 활성 조직 목록 조회 + * @return 활성 조직 목록 */ - @Deprecated - @Query("SELECT o FROM Organization o") + @Query("SELECT o FROM Organization o WHERE o.status = 'ACTIVE'") List findActiveOrganizations(); /** - * 특정 조직의 하위 조직 목록 조회 (Deprecated - 설계 B에 계층 구조 없음) - * @deprecated 설계 B에는 계층 구조가 없음 + * 특정 조직의 하위 조직 목록 조회 + * @param parentOrgId 상위 조직 ID + * @return 하위 조직 목록 */ - @Deprecated - @Query("SELECT o FROM Organization o WHERE 1=0") List findByParentOrganizationId(Long parentOrgId); /** - * 루트 조직 목록 조회 (Deprecated - 설계 B에 계층 구조 없음) - * @deprecated 설계 B에는 계층 구조가 없음 + * 루트 조직 목록 조회 (상위 조직이 없는 조직들) + * @return 루트 조직 목록 */ - @Deprecated - @Query("SELECT o FROM Organization o") + @Query("SELECT o FROM Organization o WHERE o.parentOrganization IS NULL") List findRootOrganizations(); /** @@ -92,7 +79,7 @@ public interface OrganizationRepository extends JpaRepository findTenantByOrganizationId(@Param("organizationId") Long organizationId); /** - * 조직에 테넌트가 존재하는지 확인 (1:1 관계) + * 조직에 테넌트가 존재하는지 확인 * @param organizationId 조직 ID * @return 테넌트 존재 여부 */ diff --git a/src/main/java/com/agenticcp/core/domain/organization/repository/TenantWorkerMapRepository.java b/src/main/java/com/agenticcp/core/domain/organization/repository/TenantWorkerMapRepository.java deleted file mode 100644 index 62ae99895..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/repository/TenantWorkerMapRepository.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.agenticcp.core.domain.organization.repository; - -import com.agenticcp.core.domain.organization.entity.TenantWorkerMap; -import com.agenticcp.core.domain.organization.entity.TenantWorkerMapId; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -/** - * TenantWorkerMap Repository - * - *

TenantWorkerMap 엔티티에 대한 데이터 접근을 제공합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Repository -public interface TenantWorkerMapRepository extends JpaRepository { - - /** - * 테넌트 ID로 TenantWorkerMap 목록 조회 - * - * @param tenantId 테넌트 ID - * @return TenantWorkerMap 목록 - */ - @Query("SELECT twm FROM TenantWorkerMap twm WHERE twm.tenant.id = :tenantId") - List findByTenantId(@Param("tenantId") Long tenantId); - - /** - * Worker ID로 TenantWorkerMap 목록 조회 - * - * @param workerId Worker ID - * @return TenantWorkerMap 목록 - */ - @Query("SELECT twm FROM TenantWorkerMap twm WHERE twm.worker.id = :workerId") - List findByWorkerId(@Param("workerId") Long workerId); - - /** - * 테넌트 ID와 Worker ID로 TenantWorkerMap 조회 - * - * @param tenantId 테넌트 ID - * @param workerId Worker ID - * @return TenantWorkerMap (Optional) - */ - @Query("SELECT twm FROM TenantWorkerMap twm WHERE twm.tenant.id = :tenantId AND twm.worker.id = :workerId") - Optional findByTenantIdAndWorkerId(@Param("tenantId") Long tenantId, @Param("workerId") Long workerId); - - /** - * 테넌트 ID와 Worker ID로 TenantWorkerMap 존재 여부 확인 - * - * @param tenantId 테넌트 ID - * @param workerId Worker ID - * @return 존재 여부 - */ - @Query("SELECT COUNT(twm) > 0 FROM TenantWorkerMap twm WHERE twm.tenant.id = :tenantId AND twm.worker.id = :workerId") - boolean existsByTenantIdAndWorkerId(@Param("tenantId") Long tenantId, @Param("workerId") Long workerId); - - /** - * 테넌트 ID와 Worker ID로 TenantWorkerMap 삭제 - * - * @param tenantId 테넌트 ID - * @param workerId Worker ID - */ - @Modifying - @Query("DELETE FROM TenantWorkerMap twm WHERE twm.tenant.id = :tenantId AND twm.worker.id = :workerId") - void deleteByTenantIdAndWorkerId(@Param("tenantId") Long tenantId, @Param("workerId") Long workerId); -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/repository/WorkerRepository.java b/src/main/java/com/agenticcp/core/domain/organization/repository/WorkerRepository.java deleted file mode 100644 index 325775156..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/repository/WorkerRepository.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.agenticcp.core.domain.organization.repository; - -import com.agenticcp.core.domain.organization.entity.Worker; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -/** - * Worker Repository - * - *

Worker 엔티티에 대한 데이터 접근을 제공합니다. - * 설계 C 기준: Worker는 tenant_id를 가지지 않으므로 테넌트별 조회는 CloudResourceWorkerMap을 통해 수행합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Repository -public interface WorkerRepository extends JpaRepository { - - /** - * 사용자 ID로 Worker 목록 조회 (User 기반 Worker) - * - * @param userId 사용자 ID - * @return Worker 목록 - */ - @Query("SELECT w FROM Worker w WHERE w.user.id = :userId") - List findByUserId(@Param("userId") Long userId); - - /** - * 조직 ID로 Worker 목록 조회 (Organization 기반 Worker) - * - * @param organizationId 조직 ID - * @return Worker 목록 - */ - @Query("SELECT w FROM Worker w WHERE w.organization.id = :organizationId") - List findByOrganizationId(@Param("organizationId") Long organizationId); - - /** - * 사용자 ID로 Worker 조회 (User 기반 Worker 단건) - * - * @param userId 사용자 ID - * @return Worker (Optional) - */ - @Query("SELECT w FROM Worker w WHERE w.user.id = :userId") - Optional findOneByUserId(@Param("userId") Long userId); - - /** - * 조직 ID로 Worker 조회 (Organization 기반 Worker 단건) - * - * @param organizationId 조직 ID - * @return Worker (Optional) - */ - @Query("SELECT w FROM Worker w WHERE w.organization.id = :organizationId") - Optional findOneByOrganizationId(@Param("organizationId") Long organizationId); - - /** - * 사용자 ID로 Worker 존재 여부 확인 - * - * @param userId 사용자 ID - * @return 존재 여부 - */ - @Query("SELECT COUNT(w) > 0 FROM Worker w WHERE w.user.id = :userId") - boolean existsByUserId(@Param("userId") Long userId); - - /** - * 조직 ID로 Worker 존재 여부 확인 - * - * @param organizationId 조직 ID - * @return 존재 여부 - */ - @Query("SELECT COUNT(w) > 0 FROM Worker w WHERE w.organization.id = :organizationId") - boolean existsByOrganizationId(@Param("organizationId") Long organizationId); -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/repository/WorkerRoleRepository.java b/src/main/java/com/agenticcp/core/domain/organization/repository/WorkerRoleRepository.java deleted file mode 100644 index 1381609a5..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/repository/WorkerRoleRepository.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.agenticcp.core.domain.organization.repository; - -import com.agenticcp.core.domain.organization.entity.WorkerRole; -import com.agenticcp.core.domain.organization.entity.WorkerRoleId; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -/** - * WorkerRole Repository - * - *

WorkerRole 엔티티에 대한 데이터 접근을 제공합니다. - * 설계 C 기준: tenant_id는 제거되었으며, Role이 이미 tenant_id를 가지므로 Role을 통해 테넌트 스코핑이 가능합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Repository -public interface WorkerRoleRepository extends JpaRepository { - - /** - * Worker ID로 WorkerRole 목록 조회 - * - * @param workerId Worker ID - * @return WorkerRole 목록 - */ - @Query("SELECT wr FROM WorkerRole wr WHERE wr.worker.id = :workerId AND wr.isDeleted = false") - List findByWorkerId(@Param("workerId") Long workerId); - - /** - * Worker ID와 테넌트 ID로 WorkerRole 목록 조회 - * (Role의 tenant_id를 통해 필터링) - * - * @param workerId Worker ID - * @param tenantId 테넌트 ID - * @return WorkerRole 목록 - */ - @Query("SELECT wr FROM WorkerRole wr WHERE wr.worker.id = :workerId AND wr.role.tenant.id = :tenantId AND wr.isDeleted = false") - List findByWorkerIdAndTenantId(@Param("workerId") Long workerId, @Param("tenantId") Long tenantId); - - /** - * 테넌트 ID로 WorkerRole 목록 조회 - * (Role의 tenant_id를 통해 필터링) - * - * @param tenantId 테넌트 ID - * @return WorkerRole 목록 - */ - @Query("SELECT wr FROM WorkerRole wr WHERE wr.role.tenant.id = :tenantId AND wr.isDeleted = false") - List findByTenantId(@Param("tenantId") Long tenantId); - - /** - * Worker ID와 Role ID로 WorkerRole 조회 - * - * @param workerId Worker ID - * @param roleId Role ID - * @return WorkerRole (Optional) - */ - @Query("SELECT wr FROM WorkerRole wr WHERE wr.worker.id = :workerId AND wr.role.id = :roleId AND wr.isDeleted = false") - Optional findByWorkerIdAndRoleId(@Param("workerId") Long workerId, @Param("roleId") Long roleId); - - /** - * Worker ID와 Role ID로 WorkerRole 존재 여부 확인 - * - * @param workerId Worker ID - * @param roleId Role ID - * @return 존재 여부 - */ - @Query("SELECT COUNT(wr) > 0 FROM WorkerRole wr WHERE wr.worker.id = :workerId AND wr.role.id = :roleId AND wr.isDeleted = false") - boolean existsByWorkerIdAndRoleId(@Param("workerId") Long workerId, @Param("roleId") Long roleId); - - /** - * Worker ID와 Role ID로 WorkerRole 삭제 (소프트 삭제) - * - * @param workerId Worker ID - * @param roleId Role ID - */ - @Modifying - @Query("UPDATE WorkerRole wr SET wr.isDeleted = true WHERE wr.worker.id = :workerId AND wr.role.id = :roleId") - void deleteByWorkerIdAndRoleId(@Param("workerId") Long workerId, @Param("roleId") Long roleId); -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationAwareAuthorizationService.java b/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationAwareAuthorizationService.java index 4e839cca4..e6affa514 100644 --- a/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationAwareAuthorizationService.java +++ b/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationAwareAuthorizationService.java @@ -1,7 +1,6 @@ package com.agenticcp.core.domain.organization.service; import com.agenticcp.core.domain.organization.entity.Organization; -import com.agenticcp.core.domain.organization.entity.OrganizationMember; import com.agenticcp.core.domain.organization.entity.OrganizationRole; import com.agenticcp.core.domain.organization.repository.OrganizationRepository; import com.agenticcp.core.domain.user.entity.User; @@ -10,7 +9,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; import java.util.Objects; /** @@ -30,7 +28,6 @@ public class OrganizationAwareAuthorizationService { private final OrganizationRepository organizationRepository; private final UserService userService; private final OrganizationRoleService organizationRoleService; - private final OrganizationMemberService organizationMemberService; /** * 사용자가 해당 조직에서 주어진 roleKey를 보유하는지 확인 @@ -47,18 +44,13 @@ public boolean hasRoleInOrganization(String username, Long organizationId, Strin User user = userService.getUserByUsernameOrThrow(username); Organization organization = organizationRepository.findById(organizationId) .orElse(null); - if (organization == null) { - log.debug("[OrganizationAwareAuthorizationService] hasRoleInOrganization - organization is null"); + if (organization == null || user.getOrganization() == null) { + log.debug("[OrganizationAwareAuthorizationService] hasRoleInOrganization - organization or user organization is null"); return false; } - // OrganizationMember를 통해 User-Organization 관계 확인 - List members = organizationMemberService.getOrganizationsByUserId(user.getId()); - boolean isMember = members.stream() - .anyMatch(member -> Objects.equals(member.getOrganization().getId(), organizationId)); - - if (!isMember) { - log.debug("[OrganizationAwareAuthorizationService] hasRoleInOrganization - user is not a member of organization"); + if (!Objects.equals(organization.getId(), user.getOrganization().getId())) { + log.debug("[OrganizationAwareAuthorizationService] hasRoleInOrganization - organization mismatch"); return false; } @@ -87,18 +79,12 @@ public boolean hasPermissionInOrganization(String username, Long organizationId, User user = userService.getUserByUsernameOrThrow(username); Organization organization = organizationRepository.findById(organizationId) .orElse(null); - if (organization == null) { - log.debug("[OrganizationAwareAuthorizationService] hasPermissionInOrganization - organization is null"); + if (organization == null || user.getOrganization() == null) { + log.debug("[OrganizationAwareAuthorizationService] hasPermissionInOrganization - organization or user organization is null"); return false; } - - // OrganizationMember를 통해 User-Organization 관계 확인 - List members = organizationMemberService.getOrganizationsByUserId(user.getId()); - boolean isMember = members.stream() - .anyMatch(member -> Objects.equals(member.getOrganization().getId(), organizationId)); - - if (!isMember) { - log.debug("[OrganizationAwareAuthorizationService] hasPermissionInOrganization - user is not a member of organization"); + if (!Objects.equals(organization.getId(), user.getOrganization().getId())) { + log.debug("[OrganizationAwareAuthorizationService] hasPermissionInOrganization - organization mismatch"); return false; } diff --git a/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationMemberService.java b/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationMemberService.java deleted file mode 100644 index 6edae9f72..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationMemberService.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.agenticcp.core.domain.organization.service; - -import com.agenticcp.core.common.enums.Status; -import com.agenticcp.core.common.exception.BusinessException; -import com.agenticcp.core.domain.organization.entity.Organization; -import com.agenticcp.core.domain.organization.entity.OrganizationMember; -import com.agenticcp.core.domain.organization.enums.OrganizationErrorCode; -import com.agenticcp.core.domain.organization.repository.OrganizationMemberRepository; -import com.agenticcp.core.domain.organization.repository.OrganizationRepository; -import com.agenticcp.core.domain.user.entity.User; -import com.agenticcp.core.domain.user.enums.UserErrorCode; -import com.agenticcp.core.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -/** - * OrganizationMember 서비스 - * - *

조직과 사용자 간의 관계를 관리하는 서비스입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Service -@RequiredArgsConstructor -@Slf4j -@Transactional(readOnly = true) -public class OrganizationMemberService { - - private final OrganizationMemberRepository organizationMemberRepository; - private final OrganizationRepository organizationRepository; - private final UserRepository userRepository; - - /** - * 조직에 멤버 추가 - * - * @param organizationId 조직 ID - * @param userId 사용자 ID - * @param role 조직 내 역할 (선택적) - * @return 생성된 OrganizationMember - * @throws BusinessException 조직 또는 사용자를 찾을 수 없거나 이미 멤버로 등록된 경우 - */ - @Transactional - public OrganizationMember addMember(Long organizationId, Long userId, String role) { - log.info("[OrganizationMemberService] addMember - organizationId={}, userId={}, role={}", - organizationId, userId, role); - - // 조직 존재 확인 - Organization organization = organizationRepository.findById(organizationId) - .orElseThrow(() -> new BusinessException(OrganizationErrorCode.ORGANIZATION_NOT_FOUND)); - - // 사용자 존재 확인 - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException(UserErrorCode.USER_NOT_FOUND)); - - // 이미 멤버로 등록되어 있는지 확인 - if (organizationMemberRepository.existsByOrganizationIdAndUserId(organizationId, userId)) { - throw new BusinessException(OrganizationErrorCode.ORGANIZATION_ALREADY_EXISTS, - "이미 멤버로 등록된 사용자입니다."); - } - - // OrganizationMember 생성 (설계 B: status 필드 제거됨) - OrganizationMember member = OrganizationMember.builder() - .organization(organization) - .user(user) - .role(role) - .build(); - - OrganizationMember savedMember = organizationMemberRepository.save(member); - - log.info("[OrganizationMemberService] addMember - success organizationId={}, userId={}", - organizationId, userId); - - return savedMember; - } - - /** - * 조직에서 멤버 제거 - * - * @param organizationId 조직 ID - * @param userId 사용자 ID - * @throws BusinessException 멤버를 찾을 수 없는 경우 - */ - @Transactional - public void removeMember(Long organizationId, Long userId) { - log.info("[OrganizationMemberService] removeMember - organizationId={}, userId={}", - organizationId, userId); - - OrganizationMember member = organizationMemberRepository - .findByOrganizationIdAndUserId(organizationId, userId) - .orElseThrow(() -> new BusinessException(OrganizationErrorCode.ORGANIZATION_NOT_FOUND, - "멤버를 찾을 수 없습니다.")); - - organizationMemberRepository.delete(member); - - log.info("[OrganizationMemberService] removeMember - success organizationId={}, userId={}", - organizationId, userId); - } - - /** - * 조직의 멤버 목록 조회 - * - * @param organizationId 조직 ID - * @return 멤버 목록 - */ - public List getMembers(Long organizationId) { - log.info("[OrganizationMemberService] getMembers - organizationId={}", organizationId); - return organizationMemberRepository.findByOrganizationId(organizationId); - } - - /** - * 사용자가 속한 조직 목록 조회 - * - * @param userId 사용자 ID - * @return 조직 멤버십 목록 - */ - public List getOrganizationsByUserId(Long userId) { - log.info("[OrganizationMemberService] getOrganizationsByUserId - userId={}", userId); - return organizationMemberRepository.findByUserId(userId); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationService.java b/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationService.java index b1569336a..67dd00166 100644 --- a/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationService.java +++ b/src/main/java/com/agenticcp/core/domain/organization/service/OrganizationService.java @@ -13,7 +13,6 @@ import com.agenticcp.core.domain.organization.dto.AddUserToOrganizationRequest; import com.agenticcp.core.domain.organization.dto.UserResponse; import com.agenticcp.core.domain.organization.entity.Organization; -import com.agenticcp.core.domain.organization.entity.OrganizationMember; import com.agenticcp.core.domain.organization.repository.OrganizationRepository; import com.agenticcp.core.domain.user.entity.User; import com.agenticcp.core.domain.user.repository.UserRepository; @@ -45,7 +44,6 @@ public class OrganizationService { private final OrganizationRepository organizationRepository; private final UserRepository userRepository; - private final OrganizationMemberService organizationMemberService; /** * 조직 생성 @@ -56,19 +54,40 @@ public class OrganizationService { */ @Transactional public OrganizationResponse createOrganization(CreateOrganizationRequest request) { - log.info("[OrganizationService] createOrganization - name={}", request.getOrgName()); + log.info("[OrganizationService] createOrganization - orgName={}", request.getOrgName()); - // 조직명 중복 검사 (설계 B: name 필드만 사용) + // 조직명 중복 검사 validateOrgNameUnique(request.getOrgName()); - // 조직 생성 (설계 B: name 필드만 사용) + // 조직 키 생성 (orgName 기반) + String orgKey = generateOrgKey(request.getOrgName()); + + // 조직 생성 Organization organization = Organization.builder() - .name(request.getOrgName()) + .orgKey(orgKey) + .orgName(request.getOrgName()) + .description(request.getDescription()) + .status(Status.ACTIVE) + .orgType(request.getOrgType() != null ? + Organization.OrganizationType.valueOf(request.getOrgType()) : null) + .contactEmail(request.getContactEmail()) + .contactPhone(request.getContactPhone()) + .address(request.getAddress()) + .website(request.getWebsite()) + .maxUsers(request.getMaxUsers()) + .settings(request.getSettings()) .build(); + // 상위 조직 설정 + if (request.getParentOrganizationId() != null) { + Organization parentOrg = organizationRepository.findById(request.getParentOrganizationId()) + .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 상위 조직입니다: " + request.getParentOrganizationId())); + organization.setParentOrganization(parentOrg); + } + Organization savedOrganization = organizationRepository.save(organization); - log.info("[OrganizationService] createOrganization - success id={}, name={}", - savedOrganization.getId(), savedOrganization.getName()); + log.info("[OrganizationService] createOrganization - success id={}, orgName={}", + savedOrganization.getId(), savedOrganization.getOrgName()); return OrganizationResponse.from(savedOrganization); } @@ -117,23 +136,41 @@ public List getOrganizations() { */ @Transactional public OrganizationResponse updateOrganization(Long id, UpdateOrganizationRequest request) { - log.info("[OrganizationService] updateOrganization - id={}, name={}", id, request.getOrgName()); + log.info("[OrganizationService] updateOrganization - id={}, orgName={}", id, request.getOrgName()); // 조직 존재 여부 확인 Organization organization = organizationRepository.findById(id) .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 조직입니다: " + id)); - // 조직명 중복 검사 (자신 제외) - 설계 B: name 필드만 사용 - if (!organization.getName().equals(request.getOrgName())) { + // 조직명 중복 검사 (자신 제외) + if (!organization.getOrgName().equals(request.getOrgName())) { validateOrgNameUnique(request.getOrgName()); } - // 조직 정보 수정 (설계 B: name 필드만 사용) - organization.setName(request.getOrgName()); + // 조직 정보 수정 + organization.setOrgName(request.getOrgName()); + organization.setDescription(request.getDescription()); + organization.setOrgType(request.getOrgType() != null ? + Organization.OrganizationType.valueOf(request.getOrgType()) : null); + organization.setContactEmail(request.getContactEmail()); + organization.setContactPhone(request.getContactPhone()); + organization.setAddress(request.getAddress()); + organization.setWebsite(request.getWebsite()); + organization.setMaxUsers(request.getMaxUsers()); + organization.setSettings(request.getSettings()); + + // 상위 조직 변경 + if (request.getParentOrganizationId() != null) { + Organization parentOrg = organizationRepository.findById(request.getParentOrganizationId()) + .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 상위 조직입니다: " + request.getParentOrganizationId())); + organization.setParentOrganization(parentOrg); + } else { + organization.setParentOrganization(null); + } Organization updatedOrganization = organizationRepository.save(organization); - log.info("[OrganizationService] updateOrganization - success id={}, name={}", - updatedOrganization.getId(), updatedOrganization.getName()); + log.info("[OrganizationService] updateOrganization - success id={}, orgName={}", + updatedOrganization.getId(), updatedOrganization.getOrgName()); return OrganizationResponse.from(updatedOrganization); } @@ -152,29 +189,47 @@ public void deleteOrganization(Long id) { Organization organization = organizationRepository.findById(id) .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 조직입니다: " + id)); - // 설계 B: Organization에 계층 구조가 없으므로 하위 조직 확인 불필요 + // 하위 조직 존재 여부 확인 + if (organizationRepository.existsByParentOrganizationId(id)) { + throw new BusinessException(CommonErrorCode.BAD_REQUEST, "하위 조직이 존재하는 조직은 삭제할 수 없습니다: " + id); + } // 조직 삭제 organizationRepository.delete(organization); - log.info("[OrganizationService] deleteOrganization - success id={}, name={}", - id, organization.getName()); + log.info("[OrganizationService] deleteOrganization - success id={}, orgName={}", + id, organization.getOrgName()); } /** - * 조직명 중복 검증 (설계 B: name 필드만 사용) + * 조직명 중복 검증 * - * @param name 조직명 + * @param orgName 조직명 * @throws BusinessException 조직명이 중복되는 경우 */ - private void validateOrgNameUnique(String name) { - // 설계 B: name 필드 기반으로 중복 검사 - // Repository에 existsByName 메서드가 필요하거나, findAll로 확인 - List existing = organizationRepository.findAll(); - boolean exists = existing.stream() - .anyMatch(org -> name.equals(org.getName())); - if (exists) { - throw new BusinessException(CommonErrorCode.BAD_REQUEST, "이미 존재하는 조직명입니다: " + name); + private void validateOrgNameUnique(String orgName) { + if (organizationRepository.existsByOrgName(orgName)) { + throw new BusinessException(CommonErrorCode.BAD_REQUEST, "이미 존재하는 조직명입니다: " + orgName); + } + } + + /** + * 조직 키 생성 + */ + private String generateOrgKey(String orgName) { + String baseKey = orgName.toUpperCase() + .replaceAll("[^A-Z0-9]", "_") + .replaceAll("_+", "_") + .replaceAll("^_|_$", ""); + + String orgKey = baseKey; + int counter = 1; + + while (organizationRepository.existsByOrgKey(orgKey)) { + orgKey = baseKey + "_" + counter; + counter++; } + + return orgKey; } /** @@ -192,52 +247,71 @@ public long getOrganizationCount() { /** * 특정 조직의 하위 조직 목록 조회 * - * @deprecated 설계 B: Organization에 계층 구조가 없으므로 빈 리스트 반환 * @param parentOrgId 상위 조직 ID - * @return 하위 조직 목록 (항상 빈 리스트) + * @return 하위 조직 목록 + * @throws BusinessException 상위 조직을 찾을 수 없는 경우 */ - @Deprecated public List getChildOrganizations(Long parentOrgId) { - log.warn("[OrganizationService] getChildOrganizations - Deprecated: 설계 B에는 계층 구조가 없습니다"); - return List.of(); + log.info("[OrganizationService] getChildOrganizations - parentOrgId={}", parentOrgId); + + // 상위 조직 존재 여부 확인 + organizationRepository.findById(parentOrgId) + .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 상위 조직입니다: " + parentOrgId)); + + List childOrganizations = organizationRepository.findByParentOrganizationId(parentOrgId); + List result = childOrganizations.stream() + .map(OrganizationResponse::from) + .collect(Collectors.toList()); + + log.info("[OrganizationService] getChildOrganizations - success parentOrgId={}, count={}", + parentOrgId, result.size()); + return result; } /** * 전체 조직 트리 조회 * - * @deprecated 설계 B: Organization에 계층 구조가 없으므로 단순 목록 반환 - * @return 조직 목록 + * @return 조직 계층 구조 목록 */ - @Deprecated public List getOrganizationTree() { - log.warn("[OrganizationService] getOrganizationTree - Deprecated: 설계 B에는 계층 구조가 없습니다. 단순 목록 반환"); + log.info("[OrganizationService] getOrganizationTree"); + List organizations = organizationRepository.findAll(); - return organizations.stream() - .map(org -> { - OrganizationHierarchyResponse response = OrganizationHierarchyResponse.from( - OrganizationResponse.from(org), 0, org.getName()); - response.setChildren(List.of()); - response.setChildrenCount(0); - return response; - }) + Map> childrenMap = organizations.stream() + .filter(org -> org.getParentOrganization() != null) + .collect(Collectors.groupingBy(org -> org.getParentOrganization().getId())); + + List rootOrganizations = organizationRepository.findRootOrganizations(); + + List result = rootOrganizations.stream() + .map(org -> buildHierarchyResponse(org, childrenMap, 0, org.getOrgName())) .collect(Collectors.toList()); + + log.info("[OrganizationService] getOrganizationTree - success count={}", result.size()); + return result; } /** * 조직 경로 조회 * - * @deprecated 설계 B: Organization에 계층 구조가 없으므로 단일 조직만 반환 * @param orgId 조직 ID - * @return 조직 경로 정보 (단일 조직만 포함) + * @return 조직 경로 정보 * @throws BusinessException 조직을 찾을 수 없는 경우 */ - @Deprecated public OrganizationPathResponse getOrganizationPath(Long orgId) { - log.warn("[OrganizationService] getOrganizationPath - Deprecated: 설계 B에는 계층 구조가 없습니다"); + log.info("[OrganizationService] getOrganizationPath - orgId={}", orgId); + Organization organization = organizationRepository.findById(orgId) .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 조직입니다: " + orgId)); - List path = List.of(OrganizationResponse.from(organization)); + List path = new ArrayList<>(); + Organization current = organization; + + while (current != null) { + path.add(0, OrganizationResponse.from(current)); + current = current.getParentOrganization(); + } + OrganizationPathResponse result = OrganizationPathResponse.from(path); log.info("[OrganizationService] getOrganizationPath - success orgId={}, path={}", orgId, result.getFullPath()); @@ -247,73 +321,177 @@ public OrganizationPathResponse getOrganizationPath(Long orgId) { /** * 상위 조직 목록 조회 * - * @deprecated 설계 B: Organization에 계층 구조가 없으므로 빈 리스트 반환 * @param orgId 조직 ID - * @return 상위 조직 목록 (항상 빈 리스트) + * @return 상위 조직 목록 + * @throws BusinessException 조직을 찾을 수 없는 경우 */ - @Deprecated public List getAncestors(Long orgId) { - log.warn("[OrganizationService] getAncestors - Deprecated: 설계 B에는 계층 구조가 없습니다"); - return List.of(); + log.info("[OrganizationService] getAncestors - orgId={}", orgId); + + Organization organization = organizationRepository.findById(orgId) + .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 조직입니다: " + orgId)); + + List ancestors = new ArrayList<>(); + Organization current = organization.getParentOrganization(); + + while (current != null) { + ancestors.add(0, OrganizationResponse.from(current)); + current = current.getParentOrganization(); + } + + log.info("[OrganizationService] getAncestors - success orgId={}, count={}", + orgId, ancestors.size()); + return ancestors; } /** * 하위 조직 목록 조회 (모든 레벨) * - * @deprecated 설계 B: Organization에 계층 구조가 없으므로 빈 리스트 반환 * @param orgId 조직 ID - * @return 하위 조직 목록 (항상 빈 리스트) + * @return 하위 조직 목록 + * @throws BusinessException 조직을 찾을 수 없는 경우 */ - @Deprecated public List getDescendants(Long orgId) { - log.warn("[OrganizationService] getDescendants - Deprecated: 설계 B에는 계층 구조가 없습니다"); - return List.of(); + log.info("[OrganizationService] getDescendants - orgId={}", orgId); + + Organization organization = organizationRepository.findById(orgId) + .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 조직입니다: " + orgId)); + + List descendants = new ArrayList<>(); + collectDescendants(organization, descendants); + + log.info("[OrganizationService] getDescendants - success orgId={}, count={}", + orgId, descendants.size()); + return descendants; } /** * 조직 이동 * - * @deprecated 설계 B: Organization에 계층 구조가 없으므로 동작하지 않음 * @param orgId 조직 ID * @param request 조직 이동 요청 정보 - * @return 조직 정보 (변경 없음) - * @throws BusinessException 조직을 찾을 수 없는 경우 + * @return 이동된 조직 정보 + * @throws BusinessException 조직을 찾을 수 없거나 순환 참조가 발생하는 경우 */ - @Deprecated @Transactional public OrganizationResponse moveOrganization(Long orgId, MoveOrganizationRequest request) { - log.warn("[OrganizationService] moveOrganization - Deprecated: 설계 B에는 계층 구조가 없습니다"); + log.info("[OrganizationService] moveOrganization - orgId={}, newParentId={}", + orgId, request.getNewParentId()); + Organization organization = organizationRepository.findById(orgId) .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 조직입니다: " + orgId)); - return OrganizationResponse.from(organization); + + // 새로운 상위 조직 검증 + Organization newParent = null; + if (request.getNewParentId() != null) { + newParent = organizationRepository.findById(request.getNewParentId()) + .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 상위 조직입니다: " + request.getNewParentId())); + + // 순환 참조 방지 + validateNoCircularReference(organization, newParent); + } + + organization.setParentOrganization(newParent); + Organization savedOrganization = organizationRepository.save(organization); + + log.info("[OrganizationService] moveOrganization - success orgId={}, newParentId={}", + savedOrganization.getId(), newParent != null ? newParent.getId() : null); + return OrganizationResponse.from(savedOrganization); } /** * 조직 통계 조회 * - * @deprecated 설계 B: Organization에 status 필드가 없으므로 단순 통계만 반환 * @return 조직 통계 정보 */ - @Deprecated public OrganizationStatsResponse getOrganizationStats() { - log.warn("[OrganizationService] getOrganizationStats - Deprecated: 설계 B에는 status 필드가 없습니다"); + log.info("[OrganizationService] getOrganizationStats"); + List organizations = organizationRepository.findAll(); long totalOrganizations = organizations.size(); + long activeOrganizations = organizations.stream() + .filter(org -> Status.ACTIVE.equals(org.getStatus())) + .count(); + long inactiveOrganizations = totalOrganizations - activeOrganizations; + + // 계층별 통계 + Map levelStats = new HashMap<>(); + int maxDepth = 0; + + for (Organization org : organizations) { + int level = calculateLevel(org); + levelStats.merge(level, 1L, Long::sum); + maxDepth = Math.max(maxDepth, level); + } + + List levelStatsList = levelStats.entrySet().stream() + .map(entry -> OrganizationStatsResponse.LevelStats.builder() + .level(entry.getKey()) + .count(entry.getValue()) + .description("Level " + entry.getKey()) + .build()) + .sorted(Comparator.comparing(OrganizationStatsResponse.LevelStats::getLevel)) + .collect(Collectors.toList()); OrganizationStatsResponse result = OrganizationStatsResponse.builder() .totalOrganizations(totalOrganizations) - .activeOrganizations(totalOrganizations) // status 필드가 없으므로 모두 active로 간주 - .inactiveOrganizations(0L) - .maxDepth(0) // 계층 구조 없음 - .levelStats(List.of()) + .activeOrganizations(activeOrganizations) + .inactiveOrganizations(inactiveOrganizations) + .maxDepth(maxDepth) + .levelStats(levelStatsList) .build(); - log.info("[OrganizationService] getOrganizationStats - success total={}", totalOrganizations); + log.info("[OrganizationService] getOrganizationStats - success total={}, active={}, maxDepth={}", + totalOrganizations, activeOrganizations, maxDepth); return result; } - // Helper methods - 설계 B에서는 계층 구조가 없으므로 사용되지 않음 + // Helper methods + private OrganizationHierarchyResponse buildHierarchyResponse(Organization org, + Map> childrenMap, + int level, + String path) { + List children = childrenMap.getOrDefault(org.getId(), List.of()); + List childResponses = children.stream() + .map(child -> buildHierarchyResponse(child, childrenMap, level + 1, path + " > " + child.getOrgName())) + .collect(Collectors.toList()); + + OrganizationHierarchyResponse response = OrganizationHierarchyResponse.from( + OrganizationResponse.from(org), level, path); + response.setChildren(childResponses); + response.setChildrenCount(childResponses.size()); + + return response; + } + + private void collectDescendants(Organization parent, List descendants) { + List children = organizationRepository.findByParentOrganizationId(parent.getId()); + for (Organization child : children) { + descendants.add(OrganizationResponse.from(child)); + collectDescendants(child, descendants); + } + } + + private void validateNoCircularReference(Organization org, Organization newParent) { + Organization current = newParent; + while (current != null) { + if (current.getId().equals(org.getId())) { + throw new BusinessException(CommonErrorCode.BAD_REQUEST, "순환 참조가 발생합니다: " + org.getOrgName()); + } + current = current.getParentOrganization(); + } + } + + private int calculateLevel(Organization org) { + int level = 0; + Organization current = org.getParentOrganization(); + while (current != null) { + level++; + current = current.getParentOrganization(); + } + return level; + } /** * 조직별 사용자 목록 조회 @@ -329,10 +507,10 @@ public List getOrganizationUsers(Long organizationId) { organizationRepository.findById(organizationId) .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 조직입니다: " + organizationId)); - // OrganizationMember를 통해 사용자 목록 조회 - List members = organizationMemberService.getMembers(organizationId); - List result = members.stream() - .map(member -> convertToUserResponse(member.getUser())) + // 조직의 사용자 목록 조회 + List users = userRepository.findByOrganizationId(organizationId); + List result = users.stream() + .map(this::convertToUserResponse) .collect(Collectors.toList()); log.info("[OrganizationService] getOrganizationUsers - success organizationId={}, count={}", @@ -343,8 +521,6 @@ public List getOrganizationUsers(Long organizationId) { /** * 사용자를 조직에 추가 * - *

설계 B: OrganizationMemberService를 통해 User-Organization 관계를 관리합니다.

- * * @param organizationId 조직 ID * @param request 사용자 추가 요청 정보 * @return 추가된 사용자 정보 @@ -355,21 +531,32 @@ public UserResponse addUserToOrganization(Long organizationId, AddUserToOrganiza log.info("[OrganizationService] addUserToOrganization - organizationId={}, userId={}", organizationId, request.getUserId()); - // OrganizationMemberService를 통해 멤버 추가 - OrganizationMember member = organizationMemberService.addMember( - organizationId, request.getUserId(), null); // role은 선택적이므로 null 전달 + // 조직 존재 확인 + Organization organization = organizationRepository.findById(organizationId) + .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 조직입니다: " + organizationId)); + + // 사용자 존재 확인 + User user = userRepository.findById(request.getUserId()) + .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 사용자입니다: " + request.getUserId())); + + // 이미 조직에 속한 사용자인지 확인 + if (user.getOrganization() != null && user.getOrganization().getId().equals(organizationId)) { + throw new BusinessException(CommonErrorCode.BAD_REQUEST, "이미 해당 조직에 속한 사용자입니다."); + } + + // 사용자를 조직에 추가 + user.setOrganization(organization); + User savedUser = userRepository.save(user); log.info("[OrganizationService] addUserToOrganization - success userId={}, organizationId={}", - request.getUserId(), organizationId); + savedUser.getId(), organizationId); - return convertToUserResponse(member.getUser()); + return convertToUserResponse(savedUser); } /** * 사용자를 조직에서 제거 * - *

설계 B: OrganizationMemberService를 통해 User-Organization 관계를 관리합니다.

- * * @param organizationId 조직 ID * @param userId 사용자 ID * @throws BusinessException 조직 또는 사용자를 찾을 수 없거나 사용자가 해당 조직에 속하지 않는 경우 @@ -379,8 +566,22 @@ public void removeUserFromOrganization(Long organizationId, Long userId) { log.info("[OrganizationService] removeUserFromOrganization - organizationId={}, userId={}", organizationId, userId); - // OrganizationMemberService를 통해 멤버 제거 - organizationMemberService.removeMember(organizationId, userId); + // 조직 존재 확인 + organizationRepository.findById(organizationId) + .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 조직입니다: " + organizationId)); + + // 사용자 존재 확인 + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(CommonErrorCode.NOT_FOUND, "존재하지 않는 사용자입니다: " + userId)); + + // 사용자가 해당 조직에 속하는지 확인 + if (user.getOrganization() == null || !user.getOrganization().getId().equals(organizationId)) { + throw new BusinessException(CommonErrorCode.NOT_FOUND, "사용자가 해당 조직에 속하지 않습니다: userId=" + userId + ", organizationId=" + organizationId); + } + + // 사용자를 조직에서 제거 + user.setOrganization(null); + userRepository.save(user); log.info("[OrganizationService] removeUserFromOrganization - success userId={}, organizationId={}", userId, organizationId); @@ -497,7 +698,7 @@ public Map getOrganizationTenantInfo(Long organizationId) { Map info = new HashMap<>(); info.put("organizationId", organizationId); - info.put("organizationName", organization.getName()); // 설계 B: name 필드 사용 + info.put("organizationName", organization.getOrgName()); info.put("hasTenant", tenant != null); if (tenant != null) { diff --git a/src/main/java/com/agenticcp/core/domain/organization/service/TenantWorkerService.java b/src/main/java/com/agenticcp/core/domain/organization/service/TenantWorkerService.java deleted file mode 100644 index d536c8f14..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/service/TenantWorkerService.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.agenticcp.core.domain.organization.service; - -import com.agenticcp.core.common.exception.BusinessException; -import com.agenticcp.core.domain.organization.entity.TenantWorkerMap; -import com.agenticcp.core.domain.organization.entity.Worker; -import com.agenticcp.core.domain.organization.enums.WorkerErrorCode; -import com.agenticcp.core.domain.organization.repository.TenantWorkerMapRepository; -import com.agenticcp.core.domain.organization.repository.WorkerRepository; -import com.agenticcp.core.domain.organization.repository.WorkerRoleRepository; -import com.agenticcp.core.domain.tenant.entity.Tenant; -import com.agenticcp.core.domain.tenant.repository.TenantRepository; -import com.agenticcp.core.domain.user.entity.Role; -import com.agenticcp.core.domain.user.repository.RoleRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -/** - * TenantWorkerService - * - *

테넌트와 Worker 간의 관계를 관리하는 서비스입니다. - * Shared Tenant에 Worker를 할당하고, 접근 권한을 검증합니다.

- * - * @deprecated 설계 C 기준: TenantWorkerMap은 제거되었으며, CloudResourceWorkerMap을 사용합니다. - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Deprecated -@Service -@RequiredArgsConstructor -@Slf4j -@Transactional(readOnly = true) -public class TenantWorkerService { - - private final TenantWorkerMapRepository tenantWorkerMapRepository; - private final WorkerRepository workerRepository; - private final TenantRepository tenantRepository; - private final WorkerRoleRepository workerRoleRepository; - private final RoleRepository roleRepository; - - /** - * 테넌트에 Worker 할당 (Shared Tenant용) - * - * @param tenantId 테넌트 ID - * @param workerId Worker ID - * @param accessScope 접근 범위 - * @return 생성된 TenantWorkerMap - * @throws BusinessException 테넌트, Worker를 찾을 수 없거나 이미 할당된 경우 - */ - @Transactional - public TenantWorkerMap assignWorkerToTenant(Long tenantId, Long workerId, String accessScope) { - log.info("[TenantWorkerService] assignWorkerToTenant - tenantId={}, workerId={}, accessScope={}", - tenantId, workerId, accessScope); - - // 테넌트 존재 확인 - Tenant tenant = tenantRepository.findById(tenantId) - .orElseThrow(() -> new BusinessException(WorkerErrorCode.TENANT_NOT_FOUND)); - - // Worker 존재 확인 - Worker worker = workerRepository.findById(workerId) - .orElseThrow(() -> new BusinessException(WorkerErrorCode.WORKER_NOT_FOUND)); - - // 이미 할당되어 있는지 확인 - if (tenantWorkerMapRepository.existsByTenantIdAndWorkerId(tenantId, workerId)) { - throw new BusinessException(WorkerErrorCode.TENANT_WORKER_MAP_ALREADY_EXISTS, - "이미 할당된 Worker입니다."); - } - - // TenantWorkerMap 생성 - TenantWorkerMap tenantWorkerMap = TenantWorkerMap.builder() - .tenant(tenant) - .worker(worker) - .accessScope(accessScope) - .build(); - - TenantWorkerMap savedMap = tenantWorkerMapRepository.save(tenantWorkerMap); - - log.info("[TenantWorkerService] assignWorkerToTenant - success tenantId={}, workerId={}", - tenantId, workerId); - - return savedMap; - } - - /** - * 테넌트에서 Worker 제거 - * - * @param tenantId 테넌트 ID - * @param workerId Worker ID - * @throws BusinessException TenantWorkerMap을 찾을 수 없는 경우 - */ - @Transactional - public void removeWorkerFromTenant(Long tenantId, Long workerId) { - log.info("[TenantWorkerService] removeWorkerFromTenant - tenantId={}, workerId={}", - tenantId, workerId); - - TenantWorkerMap tenantWorkerMap = tenantWorkerMapRepository - .findByTenantIdAndWorkerId(tenantId, workerId) - .orElseThrow(() -> new BusinessException(WorkerErrorCode.TENANT_WORKER_MAP_NOT_FOUND)); - - tenantWorkerMapRepository.delete(tenantWorkerMap); - - log.info("[TenantWorkerService] removeWorkerFromTenant - success tenantId={}, workerId={}", - tenantId, workerId); - } - - /** - * 테넌트의 Worker 목록 조회 - * - * @param tenantId 테넌트 ID - * @return TenantWorkerMap 목록 - */ - public List findByTenantId(Long tenantId) { - log.info("[TenantWorkerService] findByTenantId - tenantId={}", tenantId); - return tenantWorkerMapRepository.findByTenantId(tenantId); - } - - /** - * Worker가 속한 테넌트 목록 조회 - * - * @param workerId Worker ID - * @return TenantWorkerMap 목록 - */ - public List findByWorkerId(Long workerId) { - log.info("[TenantWorkerService] findByWorkerId - workerId={}", workerId); - return tenantWorkerMapRepository.findByWorkerId(workerId); - } - - /** - * Shared Tenant 접근 권한 검증 - * Worker 멤버십과 역할을 모두 확인합니다. - * - * @param userId 사용자 ID - * @param tenantId 테넌트 ID - * @param roleKey 필요한 역할 키 - * @return 접근 권한 여부 - */ - public boolean hasAccessToTenant(Long userId, Long tenantId, String roleKey) { - log.info("[TenantWorkerService] hasAccessToTenant - userId={}, tenantId={}, roleKey={}", - userId, tenantId, roleKey); - - // 1. Worker 존재 확인 (User 기반 Worker 조회) - List userWorkers = workerRepository.findByUserId(userId); - Worker worker = userWorkers.stream() - .findFirst() - .orElse(null); - - if (worker == null) { - log.debug("[TenantWorkerService] hasAccessToTenant - Worker not found"); - return false; - } - - // 2. Shared Tenant인 경우 TenantWorkerMap 확인 - Tenant tenant = tenantRepository.findById(tenantId) - .orElse(null); - - if (tenant == null) { - return false; - } - - if (tenant.getTenantType() == Tenant.TenantType.SHARED) { - boolean hasMembership = tenantWorkerMapRepository - .existsByTenantIdAndWorkerId(tenantId, worker.getId()); - - if (!hasMembership) { - log.debug("[TenantWorkerService] hasAccessToTenant - No TenantWorkerMap membership"); - return false; - } - } - - // 3. 역할 확인 (C안에서는 Role을 통해 테넌트 필터링) - List workerRoles = - workerRoleRepository.findByWorkerIdAndTenantId(worker.getId(), tenantId); - - if (workerRoles.isEmpty()) { - log.debug("[TenantWorkerService] hasAccessToTenant - No roles assigned"); - return false; - } - - // 4. 요구된 역할 확인 (테넌트별로 조회) - Role requiredRole = roleRepository.findByRoleKeyAndTenantWithPermissions(roleKey, tenant) - .orElse(null); - - if (requiredRole == null) { - log.debug("[TenantWorkerService] hasAccessToTenant - Required role not found for tenantId={}, roleKey={}", tenantId, roleKey); - return false; - } - - boolean hasRole = workerRoles.stream() - .anyMatch(wr -> wr.getRole().getId().equals(requiredRole.getId())); - - log.info("[TenantWorkerService] hasAccessToTenant - result={}, userId={}, tenantId={}", - hasRole, userId, tenantId); - - return hasRole; - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/service/WorkerRoleService.java b/src/main/java/com/agenticcp/core/domain/organization/service/WorkerRoleService.java deleted file mode 100644 index db1984fc0..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/service/WorkerRoleService.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.agenticcp.core.domain.organization.service; - -import com.agenticcp.core.common.exception.BusinessException; -import com.agenticcp.core.domain.organization.entity.Worker; -import com.agenticcp.core.domain.organization.entity.WorkerRole; -import com.agenticcp.core.domain.organization.enums.WorkerErrorCode; -import com.agenticcp.core.domain.organization.repository.WorkerRepository; -import com.agenticcp.core.domain.organization.repository.WorkerRoleRepository; -import com.agenticcp.core.domain.user.entity.Role; -import com.agenticcp.core.domain.user.repository.RoleRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -/** - * WorkerRoleService - * - *

Worker에게 역할을 부여하고 관리하는 서비스입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Service -@RequiredArgsConstructor -@Slf4j -@Transactional(readOnly = true) -public class WorkerRoleService { - - private final WorkerRoleRepository workerRoleRepository; - private final WorkerRepository workerRepository; - private final RoleRepository roleRepository; - - /** - * Worker에게 역할 부여 - * - * @param workerId Worker ID - * @param roleId Role ID (Role이 이미 tenant_id를 가짐) - * @return 생성된 WorkerRole - * @throws BusinessException Worker, Role을 찾을 수 없거나 이미 부여된 역할인 경우 - */ - @Transactional - public WorkerRole assignRole(Long workerId, Long roleId) { - log.info("[WorkerRoleService] assignRole - workerId={}, roleId={}", - workerId, roleId); - - // Worker 존재 확인 - Worker worker = workerRepository.findById(workerId) - .orElseThrow(() -> new BusinessException(WorkerErrorCode.WORKER_NOT_FOUND)); - - // Role 존재 확인 - Role role = roleRepository.findById(roleId) - .orElseThrow(() -> new BusinessException( - com.agenticcp.core.domain.user.enums.RoleErrorCode.ROLE_NOT_FOUND)); - - // 이미 부여된 역할인지 확인 - if (workerRoleRepository.existsByWorkerIdAndRoleId(workerId, roleId)) { - throw new BusinessException(WorkerErrorCode.WORKER_ROLE_ALREADY_EXISTS, - "이미 부여된 역할입니다."); - } - - // WorkerRole 생성 (tenant는 Role에서 가져옴) - WorkerRole workerRole = WorkerRole.builder() - .worker(worker) - .role(role) - .build(); - - WorkerRole savedWorkerRole = workerRoleRepository.save(workerRole); - - log.info("[WorkerRoleService] assignRole - success workerId={}, roleId={}", - workerId, roleId); - - return savedWorkerRole; - } - - /** - * Worker에서 역할 제거 - * - * @param workerId Worker ID - * @param roleId Role ID - * @throws BusinessException WorkerRole을 찾을 수 없는 경우 - */ - @Transactional - public void removeRole(Long workerId, Long roleId) { - log.info("[WorkerRoleService] removeRole - workerId={}, roleId={}", - workerId, roleId); - - WorkerRole workerRole = workerRoleRepository - .findByWorkerIdAndRoleId(workerId, roleId) - .orElseThrow(() -> new BusinessException(WorkerErrorCode.WORKER_ROLE_NOT_FOUND)); - - workerRoleRepository.deleteByWorkerIdAndRoleId(workerId, roleId); - - log.info("[WorkerRoleService] removeRole - success workerId={}, roleId={}", - workerId, roleId); - } - - /** - * Worker ID로 WorkerRole 목록 조회 - * - * @param workerId Worker ID - * @return WorkerRole 목록 - */ - public List findByWorkerId(Long workerId) { - log.info("[WorkerRoleService] findByWorkerId - workerId={}", workerId); - return workerRoleRepository.findByWorkerId(workerId); - } - -} - diff --git a/src/main/java/com/agenticcp/core/domain/organization/service/WorkerService.java b/src/main/java/com/agenticcp/core/domain/organization/service/WorkerService.java deleted file mode 100644 index 26d242ad8..000000000 --- a/src/main/java/com/agenticcp/core/domain/organization/service/WorkerService.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.agenticcp.core.domain.organization.service; - -import com.agenticcp.core.common.exception.BusinessException; -import com.agenticcp.core.domain.organization.entity.Organization; -import com.agenticcp.core.domain.organization.entity.Worker; -import com.agenticcp.core.domain.organization.enums.WorkerErrorCode; -import com.agenticcp.core.domain.organization.repository.OrganizationRepository; -import com.agenticcp.core.domain.organization.repository.WorkerRepository; -import com.agenticcp.core.domain.user.entity.User; -import com.agenticcp.core.domain.user.enums.UserErrorCode; -import com.agenticcp.core.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -/** - * Worker 서비스 - * - *

Worker의 생성, 조회를 제공합니다. - * 설계 C 기준: User와 Organization 모두 Worker로 변환 가능하며, Worker는 테넌트 독립적입니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@Service -@RequiredArgsConstructor -@Slf4j -@Transactional(readOnly = true) -public class WorkerService { - - private final WorkerRepository workerRepository; - private final UserRepository userRepository; - private final OrganizationRepository organizationRepository; - - /** - * Worker 생성 (User 기반) - * - * @param userId 사용자 ID - * @return 생성된 Worker - * @throws BusinessException 사용자를 찾을 수 없거나 이미 존재하는 Worker인 경우 - */ - @Transactional - public Worker createWorkerFromUser(Long userId) { - log.info("[WorkerService] createWorkerFromUser - userId={}", userId); - - // 사용자 존재 확인 - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException(UserErrorCode.USER_NOT_FOUND)); - - // 이미 존재하는 Worker인지 확인 - if (workerRepository.existsByUserId(userId)) { - throw new BusinessException(WorkerErrorCode.WORKER_DUPLICATE_USER); - } - - // Worker 생성 (User 기반) - Worker worker = Worker.builder() - .user(user) - .organization(null) - .build(); - - Worker savedWorker = workerRepository.save(worker); - - log.info("[WorkerService] createWorkerFromUser - success id={}, userId={}", - savedWorker.getId(), userId); - - return savedWorker; - } - - /** - * Worker 생성 (Organization 기반) - * - * @param organizationId 조직 ID - * @return 생성된 Worker - * @throws BusinessException 조직을 찾을 수 없거나 이미 존재하는 Worker인 경우 - */ - @Transactional - public Worker createWorkerFromOrganization(Long organizationId) { - log.info("[WorkerService] createWorkerFromOrganization - organizationId={}", organizationId); - - // 조직 존재 확인 - Organization organization = organizationRepository.findById(organizationId) - .orElseThrow(() -> new BusinessException(WorkerErrorCode.ORGANIZATION_NOT_FOUND)); - - // 이미 존재하는 Worker인지 확인 - if (workerRepository.existsByOrganizationId(organizationId)) { - throw new BusinessException(WorkerErrorCode.WORKER_DUPLICATE_ORGANIZATION); - } - - // Worker 생성 (Organization 기반) - Worker worker = Worker.builder() - .user(null) - .organization(organization) - .build(); - - Worker savedWorker = workerRepository.save(worker); - - log.info("[WorkerService] createWorkerFromOrganization - success id={}, organizationId={}", - savedWorker.getId(), organizationId); - - return savedWorker; - } - - /** - * 사용자 ID로 Worker 목록 조회 - * - * @param userId 사용자 ID - * @return Worker 목록 - */ - public List findByUserId(Long userId) { - log.info("[WorkerService] findByUserId - userId={}", userId); - return workerRepository.findByUserId(userId); - } - - /** - * 조직 ID로 Worker 목록 조회 - * - * @param organizationId 조직 ID - * @return Worker 목록 - */ - public List findByOrganizationId(Long organizationId) { - log.info("[WorkerService] findByOrganizationId - organizationId={}", organizationId); - return workerRepository.findByOrganizationId(organizationId); - } - - /** - * Worker 조회 - * - * @param workerId Worker ID - * @return Worker - * @throws BusinessException Worker를 찾을 수 없는 경우 - */ - public Worker findById(Long workerId) { - log.info("[WorkerService] findById - workerId={}", workerId); - return workerRepository.findById(workerId) - .orElseThrow(() -> new BusinessException(WorkerErrorCode.WORKER_NOT_FOUND)); - } -} - diff --git a/src/main/java/com/agenticcp/core/domain/security/service/AuthorizationServiceImpl.java b/src/main/java/com/agenticcp/core/domain/security/service/AuthorizationServiceImpl.java index f7d70c170..2b4bae467 100644 --- a/src/main/java/com/agenticcp/core/domain/security/service/AuthorizationServiceImpl.java +++ b/src/main/java/com/agenticcp/core/domain/security/service/AuthorizationServiceImpl.java @@ -72,11 +72,10 @@ public boolean hasPermissionForResource(String username, String resource, String @Override public void validateTenantAccess(String username, String tenantKey) { - // TODO: 설계 B - User는 전역 계정이므로 Worker를 통해 테넌트 정보를 가져와야 함 - // 현재는 임시로 예외를 발생시키지 않음 (나중에 Worker 기반으로 구현 필요) - // var user = userService.getUserByUsernameOrThrow(username); - // Worker를 통해 사용자의 테넌트 멤버십 확인 필요 - throw new AccessDeniedException("테넌트 접근 검증은 Worker 기반으로 구현 필요: " + tenantKey); + var user = userService.getUserByUsernameOrThrow(username); + if (!user.getTenant().getTenantKey().equals(tenantKey)) { + throw new AccessDeniedException("해당 테넌트에 대한 접근 권한이 없습니다"); + } } @Override diff --git a/src/main/java/com/agenticcp/core/domain/tenant/entity/Tenant.java b/src/main/java/com/agenticcp/core/domain/tenant/entity/Tenant.java index f40a42a8f..f7dc077cd 100644 --- a/src/main/java/com/agenticcp/core/domain/tenant/entity/Tenant.java +++ b/src/main/java/com/agenticcp/core/domain/tenant/entity/Tenant.java @@ -28,7 +28,7 @@ public class Tenant extends BaseEntity { @Column(name = "description") private String description; - /** 조직 (1:1 관계) */ + // Organization과의 관계 (1:1) @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "organization_id", nullable = false, unique = true) private Organization organization; @@ -78,7 +78,9 @@ public class Tenant extends BaseEntity { private LocalDateTime trialEndDate; public enum TenantType { - DEDICATED, // 전용 테넌트 - SHARED // 공유 테넌트 + INDIVIDUAL, + SMALL_BUSINESS, + ENTERPRISE, + GOVERNMENT } } diff --git a/src/main/java/com/agenticcp/core/domain/user/entity/Permission.java b/src/main/java/com/agenticcp/core/domain/user/entity/Permission.java index abbb537c7..dbacd3901 100644 --- a/src/main/java/com/agenticcp/core/domain/user/entity/Permission.java +++ b/src/main/java/com/agenticcp/core/domain/user/entity/Permission.java @@ -12,13 +12,7 @@ import java.util.List; @Entity -@Table(name = "permissions", uniqueConstraints = { - @UniqueConstraint(name = "uk_permission_tenant_action_resource", columnNames = {"tenant_id", "action", "resource"}) -}, indexes = { - @Index(name = "idx_permission_tenant", columnList = "tenant_id"), - @Index(name = "idx_permission_action", columnList = "action"), - @Index(name = "idx_permission_resource", columnList = "resource") -}) +@Table(name = "permissions") @Data @Builder @NoArgsConstructor diff --git a/src/main/java/com/agenticcp/core/domain/user/entity/Role.java b/src/main/java/com/agenticcp/core/domain/user/entity/Role.java index 941b97622..2c48e97a6 100644 --- a/src/main/java/com/agenticcp/core/domain/user/entity/Role.java +++ b/src/main/java/com/agenticcp/core/domain/user/entity/Role.java @@ -12,12 +12,7 @@ import java.util.List; @Entity -@Table(name = "roles", uniqueConstraints = { - @UniqueConstraint(name = "uk_role_tenant_key", columnNames = {"tenant_id", "role_key"}) -}, indexes = { - @Index(name = "idx_role_tenant", columnList = "tenant_id"), - @Index(name = "idx_role_key", columnList = "role_key") -}) +@Table(name = "roles") @Data @Builder @NoArgsConstructor diff --git a/src/main/java/com/agenticcp/core/domain/user/entity/User.java b/src/main/java/com/agenticcp/core/domain/user/entity/User.java index 63811c5ee..4f100a218 100644 --- a/src/main/java/com/agenticcp/core/domain/user/entity/User.java +++ b/src/main/java/com/agenticcp/core/domain/user/entity/User.java @@ -3,6 +3,8 @@ import com.agenticcp.core.common.entity.BaseEntity; import com.agenticcp.core.common.enums.Status; import com.agenticcp.core.common.enums.UserRole; +import com.agenticcp.core.domain.tenant.entity.Tenant; +import com.agenticcp.core.domain.organization.entity.Organization; import jakarta.persistence.*; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; @@ -26,6 +28,7 @@ @Table(name = "users", indexes = { @Index(name = "idx_users_username", columnList = "username"), @Index(name = "idx_users_email", columnList = "email"), + @Index(name = "idx_users_tenant", columnList = "tenant_id"), @Index(name = "idx_users_active", columnList = "status") }) @Data @@ -52,6 +55,14 @@ public class User extends BaseEntity { @Column(name = "password_hash") private String passwordHash; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tenant_id") + private Tenant tenant; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organization_id") + private Organization organization; + @Enumerated(EnumType.STRING) @Column(name = "role") private UserRole role = UserRole.VIEWER; diff --git a/src/main/java/com/agenticcp/core/domain/user/repository/UserRepository.java b/src/main/java/com/agenticcp/core/domain/user/repository/UserRepository.java index 26908108e..e7d0310e0 100644 --- a/src/main/java/com/agenticcp/core/domain/user/repository/UserRepository.java +++ b/src/main/java/com/agenticcp/core/domain/user/repository/UserRepository.java @@ -3,6 +3,7 @@ import com.agenticcp.core.domain.user.entity.User; import com.agenticcp.core.common.enums.Status; import com.agenticcp.core.common.enums.UserRole; +import com.agenticcp.core.domain.tenant.entity.Tenant; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -19,18 +20,14 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); - // 설계 B: User는 전역 계정이므로 tenant 필드 제거됨 - // @Deprecated - // List findByTenant(Tenant tenant); + List findByTenant(Tenant tenant); List findByRole(UserRole role); List findByStatus(Status status); - // 설계 B: User는 전역 계정이므로 tenant 필드 제거됨 - // @Deprecated - // @Query("SELECT u FROM User u WHERE u.tenant = :tenant AND u.status = :status AND u.isDeleted = false") - // List findActiveUsersByTenant(@Param("tenant") Tenant tenant, @Param("status") Status status); + @Query("SELECT u FROM User u WHERE u.tenant = :tenant AND u.status = :status AND u.isDeleted = false") + List findActiveUsersByTenant(@Param("tenant") Tenant tenant, @Param("status") Status status); @Query("SELECT u FROM User u WHERE u.role = :role AND u.status = :status AND u.isDeleted = false") List findActiveUsersByRole(@Param("role") UserRole role, @Param("status") Status status); @@ -41,17 +38,12 @@ public interface UserRepository extends JpaRepository { @Query("SELECT u FROM User u WHERE u.failedLoginAttempts >= :maxAttempts AND u.status = :status") List findLockedUsers(@Param("maxAttempts") Integer maxAttempts, @Param("status") Status status); - // 설계 B: User는 전역 계정이므로 tenant 필드 제거됨 - // @Deprecated - // @Query("SELECT COUNT(u) FROM User u WHERE u.tenant = :tenant AND u.status = :status AND u.isDeleted = false") - // Long countActiveUsersByTenant(@Param("tenant") Tenant tenant, @Param("status") Status status); + @Query("SELECT COUNT(u) FROM User u WHERE u.tenant = :tenant AND u.status = :status AND u.isDeleted = false") + Long countActiveUsersByTenant(@Param("tenant") Tenant tenant, @Param("status") Status status); @Query("SELECT u FROM User u WHERE u.username LIKE %:keyword% OR u.email LIKE %:keyword% OR u.name LIKE %:keyword%") List searchUsers(@Param("keyword") String keyword); - // 설계 B: User는 전역 계정이므로 organization 필드 제거됨 - // OrganizationMember를 통해 조회해야 함 - // @Deprecated - // @Query("SELECT u FROM User u WHERE u.organization.id = :organizationId AND u.isDeleted = false") - // List findByOrganizationId(@Param("organizationId") Long organizationId); + @Query("SELECT u FROM User u WHERE u.organization.id = :organizationId AND u.isDeleted = false") + List findByOrganizationId(@Param("organizationId") Long organizationId); } diff --git a/src/main/java/com/agenticcp/core/domain/user/service/UserService.java b/src/main/java/com/agenticcp/core/domain/user/service/UserService.java index 1b35dfba5..2cee93aea 100644 --- a/src/main/java/com/agenticcp/core/domain/user/service/UserService.java +++ b/src/main/java/com/agenticcp/core/domain/user/service/UserService.java @@ -80,22 +80,18 @@ public Optional getUserByEmail(String email) { return result; } - // 설계 B: User는 전역 계정이므로 tenant 필드 제거됨 - // Worker를 통해 테넌트별 사용자 조회 필요 - @Deprecated public List getUsersByTenant(Tenant tenant) { - log.warn("[UserService] getUsersByTenant - Deprecated: Worker를 통해 조회해야 함"); - // TODO: Worker를 통해 테넌트별 사용자 조회 구현 - return List.of(); + log.info("[UserService] getUsersByTenant - tenantKey={}", maskingService.maskTenantKey(tenant.getTenantKey())); + List result = userRepository.findByTenant(tenant); + log.info("[UserService] getUsersByTenant - success count={} tenantKey={}", result.size(), maskingService.maskTenantKey(tenant.getTenantKey())); + return result; } - // 설계 B: User는 전역 계정이므로 tenant 필드 제거됨 - // Worker를 통해 테넌트별 사용자 조회 필요 - @Deprecated public List getActiveUsersByTenant(Tenant tenant) { - log.warn("[UserService] getActiveUsersByTenant - Deprecated: Worker를 통해 조회해야 함"); - // TODO: Worker를 통해 테넌트별 활성 사용자 조회 구현 - return List.of(); + log.info("[UserService] getActiveUsersByTenant - tenantKey={}", maskingService.maskTenantKey(tenant.getTenantKey())); + List result = userRepository.findActiveUsersByTenant(tenant, Status.ACTIVE); + log.info("[UserService] getActiveUsersByTenant - success count={} tenantKey={}", result.size(), maskingService.maskTenantKey(tenant.getTenantKey())); + return result; } public List getUsersByRole(UserRole role) { @@ -120,13 +116,11 @@ public List getLockedUsers(int maxFailedAttempts) { return result; } - // 설계 B: User는 전역 계정이므로 tenant 필드 제거됨 - // Worker를 통해 테넌트별 사용자 수 조회 필요 - @Deprecated public Long getActiveUserCountByTenant(Tenant tenant) { - log.warn("[UserService] getActiveUserCountByTenant - Deprecated: Worker를 통해 조회해야 함"); - // TODO: Worker를 통해 테넌트별 활성 사용자 수 조회 구현 - return 0L; + log.info("[UserService] getActiveUserCountByTenant - tenantKey={}", maskingService.maskTenantKey(tenant.getTenantKey())); + Long count = userRepository.countActiveUsersByTenant(tenant, Status.ACTIVE); + log.info("[UserService] getActiveUserCountByTenant - success count={} tenantKey={}", count, maskingService.maskTenantKey(tenant.getTenantKey())); + return count; } public List searchUsers(String keyword) { @@ -159,9 +153,8 @@ public User updateUser(String username, User updatedUser) { existingUser.setEmail(updatedUser.getEmail()); existingUser.setRole(updatedUser.getRole()); existingUser.setStatus(updatedUser.getStatus()); - // 설계 B: User는 전역 계정이므로 tenant, organization 필드 제거됨 - // existingUser.setTenant(updatedUser.getTenant()); - // existingUser.setOrganization(updatedUser.getOrganization()); + existingUser.setTenant(updatedUser.getTenant()); + existingUser.setOrganization(updatedUser.getOrganization()); existingUser.setPhoneNumber(updatedUser.getPhoneNumber()); existingUser.setDepartment(updatedUser.getDepartment()); existingUser.setJobTitle(updatedUser.getJobTitle()); diff --git a/src/test/java-disabled/HealthCheckServiceTest.java b/src/test/java/com/agenticcp/core/domain/monitoring/service/HealthCheckServiceTest.java similarity index 100% rename from src/test/java-disabled/HealthCheckServiceTest.java rename to src/test/java/com/agenticcp/core/domain/monitoring/service/HealthCheckServiceTest.java diff --git a/src/test/java-disabled/MonitoringNotificationServiceTest.java b/src/test/java/com/agenticcp/core/domain/notification/service/MonitoringNotificationServiceTest.java similarity index 100% rename from src/test/java-disabled/MonitoringNotificationServiceTest.java rename to src/test/java/com/agenticcp/core/domain/notification/service/MonitoringNotificationServiceTest.java diff --git a/src/test/java/com/agenticcp/core/domain/organization/controller/OrganizationMemberControllerTest.java b/src/test/java/com/agenticcp/core/domain/organization/controller/OrganizationMemberControllerTest.java deleted file mode 100644 index a90445160..000000000 --- a/src/test/java/com/agenticcp/core/domain/organization/controller/OrganizationMemberControllerTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.agenticcp.core.domain.organization.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.organization.dto.AddMemberRequest; -import com.agenticcp.core.domain.organization.dto.OrganizationMemberResponse; -import com.agenticcp.core.domain.organization.entity.OrganizationMember; -import com.agenticcp.core.domain.organization.service.OrganizationMemberService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; - -/** - * OrganizationMemberController 단위 테스트 - * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@ExtendWith(MockitoExtension.class) -@DisplayName("OrganizationMemberController 단위 테스트") -class OrganizationMemberControllerTest { - - @Mock - private OrganizationMemberService organizationMemberService; - - @InjectMocks - private OrganizationMemberController organizationMemberController; - - private OrganizationMember testMember; - private OrganizationMemberResponse testMemberResponse; - - @BeforeEach - void setUp() { - testMember = OrganizationMember.builder() - .joinedAt(LocalDateTime.now()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - testMemberResponse = OrganizationMemberResponse.builder() - .organizationId(1L) - .organizationName("테스트 조직") - .userId(1L) - .username("testuser") - .userEmail("test@example.com") - .userName("테스트 사용자") - .role("ADMIN") - .joinedAt(LocalDateTime.now()) - .createdAt(LocalDateTime.now()) - .build(); - } - - @Nested - @DisplayName("조직 멤버 목록 조회 테스트") - class GetMembersTest { - @Test - @DisplayName("정상 조회 시 200 반환") - void getMembers_WhenValidId_ReturnsOk() { - // Given - Long organizationId = 1L; - List members = Arrays.asList(testMember); - - when(organizationMemberService.getMembers(organizationId)) - .thenReturn(members); - - // When - ResponseEntity>> response = - organizationMemberController.getMembers(organizationId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("멤버 목록을 성공적으로 조회했습니다."); - - verify(organizationMemberService).getMembers(organizationId); - } - } - - @Nested - @DisplayName("조직에 멤버 추가 테스트") - class AddMemberTest { - @Test - @DisplayName("정상 추가 시 201 반환") - void addMember_WhenValidRequest_ReturnsCreated() { - // Given - Long organizationId = 1L; - AddMemberRequest request = AddMemberRequest.builder() - .userId(1L) - .role("ADMIN") - .build(); - - when(organizationMemberService.addMember(anyLong(), anyLong(), any())) - .thenReturn(testMember); - - // When - ResponseEntity> response = - organizationMemberController.addMember(organizationId, request); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("멤버가 성공적으로 추가되었습니다."); - - verify(organizationMemberService).addMember(organizationId, request.getUserId(), request.getRole()); - } - } - - @Nested - @DisplayName("조직에서 멤버 제거 테스트") - class RemoveMemberTest { - @Test - @DisplayName("정상 제거 시 204 반환") - void removeMember_WhenValidIds_ReturnsNoContent() { - // Given - Long organizationId = 1L; - Long userId = 1L; - - doNothing().when(organizationMemberService).removeMember(anyLong(), anyLong()); - - // When - ResponseEntity response = - organizationMemberController.removeMember(organizationId, userId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); - assertThat(response.getBody()).isNull(); - - verify(organizationMemberService).removeMember(organizationId, userId); - } - } -} - diff --git a/src/test/java/com/agenticcp/core/domain/organization/controller/TenantWorkerControllerTest.java b/src/test/java/com/agenticcp/core/domain/organization/controller/TenantWorkerControllerTest.java deleted file mode 100644 index 3c00b6200..000000000 --- a/src/test/java/com/agenticcp/core/domain/organization/controller/TenantWorkerControllerTest.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.agenticcp.core.domain.organization.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.organization.dto.AssignWorkerRequest; -import com.agenticcp.core.domain.organization.dto.TenantWorkerMapResponse; -import com.agenticcp.core.domain.organization.entity.TenantWorkerMap; -import com.agenticcp.core.domain.organization.service.TenantWorkerService; -import com.agenticcp.core.domain.tenant.entity.Tenant; -import com.agenticcp.core.domain.organization.entity.Worker; -import com.agenticcp.core.domain.user.entity.User; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -/** - * TenantWorkerController 단위 테스트 - * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@ExtendWith(MockitoExtension.class) -@DisplayName("TenantWorkerController 단위 테스트") -class TenantWorkerControllerTest { - - @Mock - private TenantWorkerService tenantWorkerService; - - @InjectMocks - private TenantWorkerController tenantWorkerController; - - private TenantWorkerMap testTenantWorkerMap; - private TenantWorkerMapResponse testResponse; - - @BeforeEach - void setUp() { - User testUser = User.builder() - .username("testuser") - .email("test@example.com") - .name("테스트 사용자") - .build(); - testUser.setId(1L); - - Tenant testTenant = Tenant.builder() - .tenantKey("tenant-shared") - .tenantName("공유 테넌트") - .build(); - testTenant.setId(1L); - - Worker testWorker = Worker.builder() - .user(testUser) - .organization(null) - .build(); - testWorker.setId(1L); - - testTenantWorkerMap = TenantWorkerMap.builder() - .tenant(testTenant) - .worker(testWorker) - .accessScope("FULL") - .joinedAt(LocalDateTime.now()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - testResponse = TenantWorkerMapResponse.builder() - .tenantId(1L) - .tenantKey("tenant-shared") - .tenantName("공유 테넌트") - .workerId(1L) - .userId(1L) - .username("testuser") - .userEmail("test@example.com") - .userName("테스트 사용자") - .accessScope("FULL") - .joinedAt(LocalDateTime.now()) - .createdAt(LocalDateTime.now()) - .build(); - } - - @Nested - @DisplayName("테넌트의 Worker 목록 조회 테스트") - class GetWorkersTest { - @Test - @DisplayName("정상 조회 시 200 반환") - void getWorkers_WhenValidId_ReturnsOk() { - // Given - Long tenantId = 1L; - List maps = Arrays.asList(testTenantWorkerMap); - - when(tenantWorkerService.findByTenantId(tenantId)) - .thenReturn(maps); - - // When - ResponseEntity>> response = - tenantWorkerController.getWorkers(tenantId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("Worker 목록을 성공적으로 조회했습니다."); - assertThat(response.getBody().getData()).hasSize(1); - - verify(tenantWorkerService).findByTenantId(tenantId); - } - } - - @Nested - @DisplayName("테넌트에 Worker 할당 테스트") - class AssignWorkerTest { - @Test - @DisplayName("정상 할당 시 201 반환") - void assignWorker_WhenValidRequest_ReturnsCreated() { - // Given - Long tenantId = 1L; - AssignWorkerRequest request = AssignWorkerRequest.builder() - .workerId(1L) - .accessScope("FULL") - .build(); - - when(tenantWorkerService.assignWorkerToTenant(anyLong(), anyLong(), anyString())) - .thenReturn(testTenantWorkerMap); - - // When - ResponseEntity> response = - tenantWorkerController.assignWorker(tenantId, request); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("Worker가 성공적으로 할당되었습니다."); - assertThat(response.getBody().getData().getWorkerId()).isEqualTo(1L); - assertThat(response.getBody().getData().getTenantId()).isEqualTo(1L); - - verify(tenantWorkerService).assignWorkerToTenant(tenantId, request.getWorkerId(), request.getAccessScope()); - } - } - - @Nested - @DisplayName("테넌트에서 Worker 제거 테스트") - class RemoveWorkerTest { - @Test - @DisplayName("정상 제거 시 204 반환") - void removeWorker_WhenValidIds_ReturnsNoContent() { - // Given - Long tenantId = 1L; - Long workerId = 1L; - - doNothing().when(tenantWorkerService).removeWorkerFromTenant(anyLong(), anyLong()); - - // When - ResponseEntity response = - tenantWorkerController.removeWorker(tenantId, workerId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); - assertThat(response.getBody()).isNull(); - - verify(tenantWorkerService).removeWorkerFromTenant(tenantId, workerId); - } - } -} - diff --git a/src/test/java/com/agenticcp/core/domain/organization/controller/UserOrganizationControllerTest.java b/src/test/java/com/agenticcp/core/domain/organization/controller/UserOrganizationControllerTest.java deleted file mode 100644 index 8d42eeb81..000000000 --- a/src/test/java/com/agenticcp/core/domain/organization/controller/UserOrganizationControllerTest.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.agenticcp.core.domain.organization.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.organization.dto.OrganizationMemberResponse; -import com.agenticcp.core.domain.organization.entity.OrganizationMember; -import com.agenticcp.core.domain.organization.service.OrganizationMemberService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; - -/** - * UserOrganizationController 단위 테스트 - * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@ExtendWith(MockitoExtension.class) -@DisplayName("UserOrganizationController 단위 테스트") -class UserOrganizationControllerTest { - - @Mock - private OrganizationMemberService organizationMemberService; - - @InjectMocks - private UserOrganizationController userOrganizationController; - - private OrganizationMember testMember; - private OrganizationMemberResponse testResponse; - - @BeforeEach - void setUp() { - testMember = OrganizationMember.builder() - .joinedAt(LocalDateTime.now()) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - testResponse = OrganizationMemberResponse.builder() - .organizationId(1L) - .organizationName("테스트 조직") - .userId(1L) - .username("testuser") - .userEmail("test@example.com") - .userName("테스트 사용자") - .role("ADMIN") - .joinedAt(LocalDateTime.now()) - .createdAt(LocalDateTime.now()) - .build(); - } - - @Nested - @DisplayName("사용자의 조직 목록 조회 테스트") - class GetOrganizationsByUserIdTest { - @Test - @DisplayName("정상 조회 시 200 반환") - void getOrganizationsByUserId_WhenValidId_ReturnsOk() { - // Given - Long userId = 1L; - List members = Arrays.asList(testMember); - - when(organizationMemberService.getOrganizationsByUserId(userId)) - .thenReturn(members); - - // When - ResponseEntity>> response = - userOrganizationController.getOrganizationsByUserId(userId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("조직 목록을 성공적으로 조회했습니다."); - assertThat(response.getBody().getData()).hasSize(1); - - verify(organizationMemberService).getOrganizationsByUserId(userId); - } - - @Test - @DisplayName("조직이 없는 사용자 조회 시 빈 리스트 반환") - void getOrganizationsByUserId_WhenNoOrganizations_ReturnsEmptyList() { - // Given - Long userId = 1L; - List emptyList = Arrays.asList(); - - when(organizationMemberService.getOrganizationsByUserId(userId)) - .thenReturn(emptyList); - - // When - ResponseEntity>> response = - userOrganizationController.getOrganizationsByUserId(userId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getData()).isEmpty(); - - verify(organizationMemberService).getOrganizationsByUserId(userId); - } - } -} - diff --git a/src/test/java/com/agenticcp/core/domain/organization/controller/WorkerControllerTest.java b/src/test/java/com/agenticcp/core/domain/organization/controller/WorkerControllerTest.java deleted file mode 100644 index 0c5d75698..000000000 --- a/src/test/java/com/agenticcp/core/domain/organization/controller/WorkerControllerTest.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.agenticcp.core.domain.organization.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.organization.dto.CreateWorkerRequest; -import com.agenticcp.core.domain.organization.dto.WorkerResponse; -import com.agenticcp.core.domain.organization.entity.Worker; -import com.agenticcp.core.domain.organization.service.WorkerService; -import com.agenticcp.core.domain.tenant.entity.Tenant; -import com.agenticcp.core.domain.user.entity.User; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; - -/** - * WorkerController 단위 테스트 - * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@ExtendWith(MockitoExtension.class) -@DisplayName("WorkerController 단위 테스트") -class WorkerControllerTest { - - @Mock - private WorkerService workerService; - - @InjectMocks - private WorkerController workerController; - - private Worker testWorker; - private WorkerResponse testWorkerResponse; - - @BeforeEach - void setUp() { - User testUser = User.builder() - .username("testuser") - .email("test@example.com") - .name("테스트 사용자") - .build(); - testUser.setId(1L); - - Tenant testTenant = Tenant.builder() - .tenantKey("tenant-dev") - .tenantName("개발 테넌트") - .build(); - testTenant.setId(1L); - - testWorker = Worker.builder() - .user(testUser) - .organization(null) - .build(); - testWorker.setId(1L); - testWorker.setCreatedAt(LocalDateTime.now()); - testWorker.setUpdatedAt(LocalDateTime.now()); - - testWorkerResponse = WorkerResponse.builder() - .id(1L) - .userId(1L) - .username("testuser") - .userEmail("test@example.com") - .userName("테스트 사용자") - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - } - - @Nested - @DisplayName("Worker 생성 테스트") - class CreateWorkerTest { - @Test - @DisplayName("정상 생성 시 201 반환") - void createWorkerFromUser_WhenValidRequest_ReturnsCreated() { - // Given - Long userId = 1L; - - when(workerService.createWorkerFromUser(anyLong())) - .thenReturn(testWorker); - - // When - ResponseEntity> response = - workerController.createWorkerFromUser(userId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("Worker가 성공적으로 생성되었습니다."); - assertThat(response.getBody().getData().getUserId()).isEqualTo(1L); - - verify(workerService).createWorkerFromUser(userId); - } - } - - @Nested - @DisplayName("사용자의 Worker 목록 조회 테스트") - class GetWorkersByUserIdTest { - @Test - @DisplayName("정상 조회 시 200 반환") - void getWorkersByUserId_WhenValidId_ReturnsOk() { - // Given - Long userId = 1L; - List workers = Arrays.asList(testWorker); - - when(workerService.findByUserId(userId)) - .thenReturn(workers); - - // When - ResponseEntity>> response = - workerController.getWorkersByUserId(userId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("Worker 목록을 성공적으로 조회했습니다."); - assertThat(response.getBody().getData()).hasSize(1); - - verify(workerService).findByUserId(userId); - } - } - - @Nested - @DisplayName("Worker 조회 테스트") - class GetWorkerTest { - @Test - @DisplayName("정상 조회 시 200 반환") - void getWorker_WhenValidIds_ReturnsOk() { - // Given - Long userId = 1L; - Long workerId = 1L; - - when(workerService.findById(workerId)) - .thenReturn(testWorker); - - // When - ResponseEntity> response = - workerController.getWorker(userId, workerId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("Worker 정보를 성공적으로 조회했습니다."); - assertThat(response.getBody().getData().getId()).isEqualTo(1L); - - verify(workerService).findById(workerId); - } - } -} - diff --git a/src/test/java/com/agenticcp/core/domain/organization/controller/WorkerRoleControllerTest.java b/src/test/java/com/agenticcp/core/domain/organization/controller/WorkerRoleControllerTest.java deleted file mode 100644 index 41aeb4713..000000000 --- a/src/test/java/com/agenticcp/core/domain/organization/controller/WorkerRoleControllerTest.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.agenticcp.core.domain.organization.controller; - -import com.agenticcp.core.common.dto.exception.ApiResponse; -import com.agenticcp.core.domain.organization.dto.AssignRoleRequest; -import com.agenticcp.core.domain.organization.dto.WorkerRoleResponse; -import com.agenticcp.core.domain.organization.entity.WorkerRole; -import com.agenticcp.core.domain.organization.service.WorkerRoleService; -import com.agenticcp.core.domain.tenant.entity.Tenant; -import com.agenticcp.core.domain.organization.entity.Worker; -import com.agenticcp.core.domain.user.entity.Role; -import com.agenticcp.core.domain.user.entity.User; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; - -/** - * WorkerRoleController 단위 테스트 - * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@ExtendWith(MockitoExtension.class) -@DisplayName("WorkerRoleController 단위 테스트") -class WorkerRoleControllerTest { - - @Mock - private WorkerRoleService workerRoleService; - - @InjectMocks - private WorkerRoleController workerRoleController; - - private WorkerRole testWorkerRole; - private WorkerRoleResponse testResponse; - - @BeforeEach - void setUp() { - User testUser = User.builder() - .username("testuser") - .email("test@example.com") - .name("테스트 사용자") - .build(); - testUser.setId(1L); - - Tenant testTenant = Tenant.builder() - .tenantKey("tenant-dev") - .tenantName("개발 테넌트") - .build(); - testTenant.setId(1L); - - Worker testWorker = Worker.builder() - .user(testUser) - .organization(null) - .build(); - testWorker.setId(1L); - - Role testRole = Role.builder() - .roleKey("ADMIN") - .roleName("관리자") - .tenant(testTenant) - .build(); - testRole.setId(1L); - - testWorkerRole = WorkerRole.builder() - .worker(testWorker) - .role(testRole) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - testResponse = WorkerRoleResponse.builder() - .workerId(1L) - .userId(1L) - .username("testuser") - .roleId(1L) - .roleKey("ADMIN") - .roleName("관리자") - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - } - - @Nested - @DisplayName("Worker의 역할 목록 조회 테스트") - class GetRolesTest { - @Test - @DisplayName("정상 조회 시 200 반환") - void getRoles_WhenValidId_ReturnsOk() { - // Given - Long workerId = 1L; - List roles = Arrays.asList(testWorkerRole); - - when(workerRoleService.findByWorkerId(workerId)) - .thenReturn(roles); - - // When - ResponseEntity>> response = - workerRoleController.getRoles(workerId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("역할 목록을 성공적으로 조회했습니다."); - assertThat(response.getBody().getData()).hasSize(1); - - verify(workerRoleService).findByWorkerId(workerId); - } - } - - @Nested - @DisplayName("Worker에게 역할 부여 테스트") - class AssignRoleTest { - @Test - @DisplayName("정상 부여 시 200 반환") - void assignRole_WhenValidRequest_ReturnsOk() { - // Given - Long workerId = 1L; - AssignRoleRequest request = AssignRoleRequest.builder() - .roleId(1L) - .build(); - - when(workerRoleService.assignRole(anyLong(), anyLong())) - .thenReturn(testWorkerRole); - - // When - ResponseEntity> response = - workerRoleController.assignRole(workerId, request); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody().isSuccess()).isTrue(); - assertThat(response.getBody().getMessage()).isEqualTo("역할이 성공적으로 부여되었습니다."); - assertThat(response.getBody().getData().getWorkerId()).isEqualTo(1L); - assertThat(response.getBody().getData().getRoleId()).isEqualTo(1L); - - verify(workerRoleService).assignRole(workerId, request.getRoleId()); - } - } - - @Nested - @DisplayName("Worker에서 역할 제거 테스트") - class RemoveRoleTest { - @Test - @DisplayName("정상 제거 시 204 반환") - void removeRole_WhenValidParams_ReturnsNoContent() { - // Given - Long workerId = 1L; - Long roleId = 1L; - - doNothing().when(workerRoleService).removeRole(anyLong(), anyLong()); - - // When - ResponseEntity response = - workerRoleController.removeRole(workerId, roleId); - - // Then - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); - assertThat(response.getBody()).isNull(); - - verify(workerRoleService).removeRole(workerId, roleId); - } - } -} - diff --git a/src/test/java-disabled/OrganizationAwareAuthorizationServiceTest.java b/src/test/java/com/agenticcp/core/domain/organization/service/OrganizationAwareAuthorizationServiceTest.java similarity index 100% rename from src/test/java-disabled/OrganizationAwareAuthorizationServiceTest.java rename to src/test/java/com/agenticcp/core/domain/organization/service/OrganizationAwareAuthorizationServiceTest.java diff --git a/src/test/java-disabled/OrganizationHierarchyServiceTest.java b/src/test/java/com/agenticcp/core/domain/organization/service/OrganizationHierarchyServiceTest.java similarity index 100% rename from src/test/java-disabled/OrganizationHierarchyServiceTest.java rename to src/test/java/com/agenticcp/core/domain/organization/service/OrganizationHierarchyServiceTest.java diff --git a/src/test/java-disabled/OrganizationRoleServiceTest.java b/src/test/java/com/agenticcp/core/domain/organization/service/OrganizationRoleServiceTest.java similarity index 100% rename from src/test/java-disabled/OrganizationRoleServiceTest.java rename to src/test/java/com/agenticcp/core/domain/organization/service/OrganizationRoleServiceTest.java diff --git a/src/test/java-disabled/OrganizationServiceTest.java b/src/test/java/com/agenticcp/core/domain/organization/service/OrganizationServiceTest.java similarity index 100% rename from src/test/java-disabled/OrganizationServiceTest.java rename to src/test/java/com/agenticcp/core/domain/organization/service/OrganizationServiceTest.java diff --git a/src/test/java-disabled/OrganizationTenantServiceTest.java b/src/test/java/com/agenticcp/core/domain/organization/service/OrganizationTenantServiceTest.java similarity index 100% rename from src/test/java-disabled/OrganizationTenantServiceTest.java rename to src/test/java/com/agenticcp/core/domain/organization/service/OrganizationTenantServiceTest.java diff --git a/src/test/java/com/agenticcp/core/domain/organization/service/WorkerServiceIntegrationTest.java b/src/test/java/com/agenticcp/core/domain/organization/service/WorkerServiceIntegrationTest.java deleted file mode 100644 index 76004c546..000000000 --- a/src/test/java/com/agenticcp/core/domain/organization/service/WorkerServiceIntegrationTest.java +++ /dev/null @@ -1,327 +0,0 @@ -package com.agenticcp.core.domain.organization.service; - -import com.agenticcp.core.common.enums.Status; -import com.agenticcp.core.common.exception.BusinessException; -import com.agenticcp.core.domain.organization.entity.Organization; -import com.agenticcp.core.domain.organization.entity.OrganizationMember; -import com.agenticcp.core.domain.organization.repository.OrganizationMemberRepository; -import com.agenticcp.core.domain.organization.repository.OrganizationRepository; -import com.agenticcp.core.domain.tenant.entity.Tenant; -import com.agenticcp.core.domain.tenant.repository.TenantRepository; -import com.agenticcp.core.domain.user.entity.Role; -import com.agenticcp.core.domain.user.entity.User; -import com.agenticcp.core.domain.user.repository.RoleRepository; -import com.agenticcp.core.domain.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -import static org.assertj.core.api.Assertions.*; - -/** - * Worker 서비스 통합 테스트 - * - *

설계 C 기준: User/Organization 기반 Worker 생성 및 리소스 단위 접근 권한 관리 시나리오를 검증합니다.

- * - * @author AgenticCP Team - * @version 1.0.0 - * @since 2025-12-14 - */ -@SpringBootTest -@ActiveProfiles("test") -@Transactional -@DisplayName("Worker 서비스 통합 테스트") -class WorkerServiceIntegrationTest { - - @Autowired - private WorkerService workerService; - - @Autowired - private TenantWorkerService tenantWorkerService; - - @Autowired - private WorkerRoleService workerRoleService; - - @Autowired - private UserRepository userRepository; - - @Autowired - private TenantRepository tenantRepository; - - @Autowired - private OrganizationRepository organizationRepository; - - @Autowired - private OrganizationMemberRepository organizationMemberRepository; - - @Autowired - private OrganizationMemberService organizationMemberService; - - @Autowired - private RoleRepository roleRepository; - - private User testUser; - private Tenant dedicatedTenant; - private Tenant sharedTenant; - private Organization testOrganization; - private Role adminRole; - private Role viewerRole; - private Role sharedAdminRole; - - @BeforeEach - void setUp() { - // 테스트 데이터 준비 - testUser = User.builder() - .username("testuser") - .email("test@example.com") - .name("테스트 사용자") - .status(Status.ACTIVE) - .build(); - testUser = userRepository.save(testUser); - - // Dedicated Tenant용 Organization - Organization dedicatedOrg = Organization.builder() - .name("전용 테넌트 조직") - .build(); - dedicatedOrg = organizationRepository.save(dedicatedOrg); - - // Shared Tenant용 Organization - Organization sharedOrg = Organization.builder() - .name("공유 테넌트 조직") - .build(); - sharedOrg = organizationRepository.save(sharedOrg); - - testOrganization = dedicatedOrg; // 기본 조직으로 사용 - - // 설계 B: Tenant는 organization 필드로 1:1 관계 - dedicatedTenant = Tenant.builder() - .tenantKey("dedicated-tenant") - .tenantName("전용 테넌트") - .organization(dedicatedOrg) // Dedicated Tenant는 organization 사용 (1:1 관계) - .tenantType(Tenant.TenantType.DEDICATED) - .status(Status.ACTIVE) - .build(); - dedicatedTenant = tenantRepository.save(dedicatedTenant); - - // Shared Tenant도 organization을 가짐 (1:1 관계) - sharedTenant = Tenant.builder() - .tenantKey("shared-tenant") - .tenantName("공유 테넌트") - .organization(sharedOrg) // Shared Tenant도 organization 필요 (1:1 관계) - .tenantType(Tenant.TenantType.SHARED) - .status(Status.ACTIVE) - .build(); - sharedTenant = tenantRepository.save(sharedTenant); - - adminRole = Role.builder() - .roleKey("admin") - .roleName("관리자") - .tenant(dedicatedTenant) - .status(Status.ACTIVE) - .build(); - adminRole = roleRepository.save(adminRole); - - viewerRole = Role.builder() - .roleKey("viewer") - .roleName("조회자") - .tenant(dedicatedTenant) - .status(Status.ACTIVE) - .build(); - viewerRole = roleRepository.save(viewerRole); - - // Shared Tenant용 Role도 생성 - sharedAdminRole = Role.builder() - .roleKey("admin") - .roleName("관리자") - .tenant(sharedTenant) - .status(Status.ACTIVE) - .build(); - sharedAdminRole = roleRepository.save(sharedAdminRole); - } - - @Nested - @DisplayName("시나리오 1: User가 Worker로 생성되는 경우") - class CreateWorkerScenario { - - @Test - @DisplayName("User 기반 Worker 생성 성공") - void createWorkerFromUser_Success() { - // When - var worker = workerService.createWorkerFromUser(testUser.getId()); - - // Then - assertThat(worker).isNotNull(); - assertThat(worker.getUser().getId()).isEqualTo(testUser.getId()); - assertThat(worker.getOrganization()).isNull(); - assertThat(worker.getId()).isNotNull(); - } - - @Test - @DisplayName("같은 User로 중복 생성 시 예외 발생") - void createWorkerFromUser_Duplicate_ThrowsException() { - // Given - workerService.createWorkerFromUser(testUser.getId()); - - // When & Then - assertThatThrownBy(() -> workerService.createWorkerFromUser(testUser.getId())) - .isInstanceOf(BusinessException.class) - .hasMessageContaining("이미 Worker가 생성되었습니다"); - } - - @Test - @DisplayName("존재하지 않는 User로 Worker 생성 시 예외 발생") - void createWorkerFromUser_WithNonExistentUser_ThrowsException() { - // When & Then - assertThatThrownBy(() -> workerService.createWorkerFromUser(999L)) - .isInstanceOf(Exception.class) - .hasMessageContaining("사용자를 찾을 수 없습니다"); - } - - @Test - @DisplayName("Organization 기반 Worker 생성 성공") - void createWorkerFromOrganization_Success() { - // When - var worker = workerService.createWorkerFromOrganization(testOrganization.getId()); - - // Then - assertThat(worker).isNotNull(); - assertThat(worker.getOrganization().getId()).isEqualTo(testOrganization.getId()); - assertThat(worker.getUser()).isNull(); - assertThat(worker.getId()).isNotNull(); - } - } - - @Nested - @DisplayName("시나리오 2: Worker에게 역할 부여 (WorkerRole) - C안 기준") - class AssignRoleToWorkerScenario { - - @Test - @DisplayName("Worker에게 역할 부여 성공") - void assignRoleToWorker_Success() { - // Given - var worker = workerService.createWorkerFromUser(testUser.getId()); - - // When - var workerRole = workerRoleService.assignRole(worker.getId(), adminRole.getId()); - - // Then - assertThat(workerRole).isNotNull(); - assertThat(workerRole.getWorker().getId()).isEqualTo(worker.getId()); - assertThat(workerRole.getRole().getId()).isEqualTo(adminRole.getId()); - } - - @Test - @DisplayName("Worker에게 여러 역할 부여 가능") - void assignRoleToWorker_MultipleRoles_Success() { - // Given - var worker = workerService.createWorkerFromUser(testUser.getId()); - - // When - var workerRole1 = workerRoleService.assignRole(worker.getId(), adminRole.getId()); - var workerRole2 = workerRoleService.assignRole(worker.getId(), viewerRole.getId()); - - // Then - assertThat(workerRole1).isNotNull(); - assertThat(workerRole2).isNotNull(); - - var roles = workerRoleService.findByWorkerId(worker.getId()); - assertThat(roles).hasSize(2); - } - - @Test - @DisplayName("같은 Worker에게 같은 역할 중복 부여 시 예외 발생") - void assignRoleToWorker_DuplicateRole_ThrowsException() { - // Given - var worker = workerService.createWorkerFromUser(testUser.getId()); - workerRoleService.assignRole(worker.getId(), adminRole.getId()); - - // When & Then - assertThatThrownBy(() -> workerRoleService.assignRole(worker.getId(), adminRole.getId())) - .isInstanceOf(Exception.class) - .hasMessageContaining("이미 부여된 역할입니다"); - } - - @Test - @DisplayName("Worker 역할 제거 성공") - void removeRoleFromWorker_Success() { - // Given - var worker = workerService.createWorkerFromUser(testUser.getId()); - workerRoleService.assignRole(worker.getId(), adminRole.getId()); - - // When - workerRoleService.removeRole(worker.getId(), adminRole.getId()); - - // Then - var roles = workerRoleService.findByWorkerId(worker.getId()); - assertThat(roles).isEmpty(); - } - } - - - @Nested - @DisplayName("시나리오 4: OrganizationMember를 통한 User-Organization 관계 관리") - class OrganizationMemberScenario { - - @Test - @DisplayName("User를 Organization에 멤버로 추가 성공") - void addUserToOrganization_Success() { - // When - var member = organizationMemberService.addMember( - testOrganization.getId(), testUser.getId(), "ADMIN"); - - // Then - assertThat(member).isNotNull(); - assertThat(member.getOrganization().getId()).isEqualTo(testOrganization.getId()); - assertThat(member.getUser().getId()).isEqualTo(testUser.getId()); - assertThat(member.getRole()).isEqualTo("ADMIN"); - } - - @Test - @DisplayName("같은 User를 같은 Organization에 중복 추가 시 예외 발생") - void addUserToOrganization_Duplicate_ThrowsException() { - // Given - organizationMemberService.addMember(testOrganization.getId(), testUser.getId(), "ADMIN"); - - // When & Then - assertThatThrownBy(() -> organizationMemberService.addMember( - testOrganization.getId(), testUser.getId(), "VIEWER")) - .isInstanceOf(Exception.class) - .hasMessageContaining("이미 멤버로 등록된 사용자입니다"); - } - } - - - @Nested - @DisplayName("시나리오 6: 복합 시나리오 - 전체 플로우") - class ComplexScenario { - - @Test - @DisplayName("User → Worker 생성 → Tenant 할당 → 역할 부여 전체 플로우") - void completeFlow_Success() { - // Step 1: User를 Organization에 멤버로 추가 - var member = organizationMemberService.addMember( - testOrganization.getId(), testUser.getId(), "ADMIN"); - assertThat(member).isNotNull(); - - // Step 2: User 기반으로 Worker 생성 - var worker = workerService.createWorkerFromUser(testUser.getId()); - assertThat(worker).isNotNull(); - - // Step 3: Worker에게 역할 부여 - var workerRole = workerRoleService.assignRole(worker.getId(), adminRole.getId()); - assertThat(workerRole).isNotNull(); - - // Step 4: Worker 조회 - var workers = workerService.findByUserId(testUser.getId()); - assertThat(workers).hasSize(1); - } - } -} - diff --git a/src/test/java-disabled/AuthorizationServiceImplTest.java b/src/test/java/com/agenticcp/core/domain/security/service/AuthorizationServiceImplTest.java similarity index 100% rename from src/test/java-disabled/AuthorizationServiceImplTest.java rename to src/test/java/com/agenticcp/core/domain/security/service/AuthorizationServiceImplTest.java