68c1cb5b14
CRITICAL: - searchUsers 회사/role 격리 가드 추가 (멀티테넌시 침해 차단) - setPrimaryDept 멤버십 검증 추가 (주부서 데이터 손상 방지) - parent_dept_code cross-tenant 검증 (validateParent 헬퍼) HIGH: - updateDepartment SQL WHERE 에 DELETED_AT IS NULL 추가 (silent corruption 방지) - update/restore 부서명 중복 검증 추가 - 글로벌 부서 (*) write 작업 SUPER_ADMIN 전용 가드 - 부서코드 자동 생성으로 강제 (사용자 입력 받지 않음) - 회사 변경 시 상세 패널 초기화 - handleMove 부분 실패 시 화면 동기화 - 검색 시 부모 체인 자동 포함 (broken tree 수정) - start_date 기본값 today 강제 제거 MEDIUM: - 멤버 fetch cancellation flag - 삭제 다이얼로그 dept_code 클로저 캡처 - isDirty 시 X 버튼 폼 초기화 경고 - 변경이력 버튼 disabled (백엔드 API 미구현) - 일괄등록 실패 상세 모달 (라인 + 사유) - LIKE 와일드카드 ESCAPE 적용 - nullIfBlank 에 trim 통합 LOW + 새 기능: - 부서원 추가/제거 UI 신규 구현 (UserSearchModal) - selectDeptMembers LEFT JOIN 으로 변경 - DepartmentPicker allowRoot 옵션 (최상위로 이동) - expandAll 전체 departments 사용 - dead code 정리 DB: - RUN_085 마이그레이션: DEPT_INFO partial UNIQUE + USER_DEPT UNIQUE - 모든 active 테넌트 DB (siflex/test01/test02_invyone) 적용 완료 Breaking changes: - 일괄등록 CSV 4컬럼 → 3컬럼 (부서명,상위부서,유형) - 부서코드 입력란 제거 (자동 부여 DEPT_n) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
489 lines
24 KiB
Java
489 lines
24 KiB
Java
package com.erp.service;
|
|
|
|
import com.erp.common.BaseService;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
@Service
|
|
@Slf4j
|
|
public class DepartmentService extends BaseService {
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// 부서 CRUD
|
|
// ──────────────────────────────────────────────────
|
|
|
|
public List<Map<String, Object>> getDepartments(String companyCode) {
|
|
return getDepartments(companyCode, false);
|
|
}
|
|
|
|
/** soft-delete 대응 — includeDeleted=true 면 DELETED_AT 부서도 포함 */
|
|
public List<Map<String, Object>> getDepartments(String companyCode, boolean includeDeleted) {
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put("company_code", companyCode);
|
|
params.put("include_deleted", includeDeleted);
|
|
List<Map<String, Object>> departments = sqlSession.selectList("department.selectDepartments", params);
|
|
|
|
// member_count를 int로 변환
|
|
for (Map<String, Object> dept : departments) {
|
|
Object cnt = dept.get("member_count");
|
|
if (cnt != null) {
|
|
dept.put("member_count", ((Number) cnt).intValue());
|
|
} else {
|
|
dept.put("member_count", 0);
|
|
}
|
|
}
|
|
return departments;
|
|
}
|
|
|
|
/** active 부서만 반환. deleted 면 null. 복구 흐름은 getDepartmentIncludingDeleted 사용 */
|
|
public Map<String, Object> getDepartment(String deptCode) {
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put("dept_code", deptCode);
|
|
return sqlSession.selectOne("department.selectDepartmentByCode", params);
|
|
}
|
|
|
|
/** deleted 부서까지 포함 — 복구 검증 / 부모 deleted 체크 등 internal 흐름용 */
|
|
public Map<String, Object> getDepartmentIncludingDeleted(String deptCode) {
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put("dept_code", deptCode);
|
|
return sqlSession.selectOne("department.selectDepartmentByCodeIncludingDeleted", params);
|
|
}
|
|
|
|
@Transactional
|
|
public Map<String, Object> createDepartment(String companyCode, Map<String, Object> body) {
|
|
// 프론트엔드는 snake_case로 전송 (Node.js 호환)
|
|
String deptName = trimString(bodyParam(body, "dept_name", "dept_name"));
|
|
if (deptName == null || deptName.isEmpty()) {
|
|
throw new IllegalArgumentException("부서명을 입력해주세요.");
|
|
}
|
|
|
|
// 중복 부서명 확인
|
|
Map<String, Object> dupParams = new HashMap<>();
|
|
dupParams.put("company_code", companyCode);
|
|
dupParams.put("dept_name", deptName);
|
|
Map<String, Object> duplicate = sqlSession.selectOne("department.selectDuplicateDeptName", dupParams);
|
|
if (duplicate != null) {
|
|
throw new DuplicateDeptNameException("\"" + deptName + "\" 부서가 이미 존재합니다.");
|
|
}
|
|
|
|
// 회사명 조회
|
|
Map<String, Object> companyParams = new HashMap<>();
|
|
companyParams.put("company_code", companyCode);
|
|
Map<String, Object> company = sqlSession.selectOne("department.selectCompanyName", companyParams);
|
|
String companyName = (company != null && company.get("company_name") != null)
|
|
? (String) company.get("company_name")
|
|
: companyCode;
|
|
|
|
// parent_dept_code cross-tenant / 존재 / 삭제 검증
|
|
Object parentObj = nullIfBlank(bodyParam(body, "parent_dept_code", "parent_dept_code"));
|
|
String parentCode = parentObj != null ? parentObj.toString() : null;
|
|
validateParent(parentCode, companyCode);
|
|
|
|
// 부서 코드 자동 생성 — 사용자 입력 받지 않음 (정책 변경 2026-05-08)
|
|
// 재시도 로직 (race condition 대비, 최대 3회)
|
|
String deptCode = null;
|
|
for (int attempt = 0; attempt < 3; attempt++) {
|
|
Map<String, Object> codeResult = sqlSession.selectOne("department.selectNextDeptNumber", null);
|
|
long nextNumber = codeResult != null && codeResult.get("next_number") != null
|
|
? ((Number) codeResult.get("next_number")).longValue()
|
|
: 1L;
|
|
String candidate = "DEPT_" + nextNumber;
|
|
Map<String, Object> existing = sqlSession.selectOne("department.selectDepartmentByCodeIncludingDeleted",
|
|
Map.of("dept_code", candidate));
|
|
if (existing == null) {
|
|
deptCode = candidate;
|
|
break;
|
|
}
|
|
}
|
|
if (deptCode == null) {
|
|
throw new IllegalStateException("부서 코드 생성 실패 (동시 생성 충돌). 잠시 후 다시 시도해주세요.");
|
|
}
|
|
|
|
// 부서 생성 (전체 필드)
|
|
Map<String, Object> insertParams = new HashMap<>();
|
|
insertParams.put("dept_code", deptCode);
|
|
insertParams.put("dept_name", deptName);
|
|
insertParams.put("company_code", companyCode);
|
|
insertParams.put("company_name", companyName);
|
|
insertParams.put("parent_dept_code", parentCode);
|
|
insertParams.put("short_name", nullIfBlank(bodyParam(body, "short_name", "short_name")));
|
|
insertParams.put("dept_type", bodyParam(body, "dept_type", "dept_type"));
|
|
insertParams.put("org_system", nullIfBlank(bodyParam(body, "org_system", "org_system")));
|
|
insertParams.put("approval_manager", nullIfBlank(bodyParam(body, "approval_manager", "approval_manager")));
|
|
insertParams.put("dept_manager", nullIfBlank(bodyParam(body, "dept_manager", "dept_manager")));
|
|
insertParams.put("zipcode", nullIfBlank(bodyParam(body, "zipcode", "zipcode")));
|
|
insertParams.put("address1", nullIfBlank(bodyParam(body, "address1", "address1")));
|
|
insertParams.put("address2", nullIfBlank(bodyParam(body, "address2", "address2")));
|
|
insertParams.put("start_date", nullIfBlank(bodyParam(body, "start_date", "start_date")));
|
|
insertParams.put("end_date", nullIfBlank(bodyParam(body, "end_date", "end_date")));
|
|
insertParams.put("sort_order", bodyParam(body, "sort_order", "sort_order"));
|
|
insertParams.put("status", bodyParam(body, "status", "status"));
|
|
// dept_info 추가 필드 (location 코드만 유지 — V019 정리 후)
|
|
insertParams.put("location", nullIfBlank(bodyParam(body, "location", "location")));
|
|
sqlSession.insert("department.insertDepartment", insertParams);
|
|
|
|
log.info("부서 생성 성공: deptCode={}, deptName={}", deptCode, deptName);
|
|
|
|
Map<String, Object> findParams = new HashMap<>();
|
|
findParams.put("dept_code", deptCode);
|
|
return sqlSession.selectOne("department.selectDepartmentByCode", findParams);
|
|
}
|
|
|
|
@Transactional
|
|
public Map<String, Object> updateDepartment(String deptCode, Map<String, Object> body) {
|
|
String deptName = trimString(bodyParam(body, "dept_name", "dept_name"));
|
|
if (deptName == null || deptName.isEmpty()) {
|
|
throw new IllegalArgumentException("부서명을 입력해주세요.");
|
|
}
|
|
|
|
// 본인 dept 의 company_code 조회 (validateParent + 중복명 검증에 사용)
|
|
Map<String, Object> existingDept = sqlSession.selectOne(
|
|
"department.selectDepartmentByCodeIncludingDeleted",
|
|
Map.of("dept_code", deptCode)
|
|
);
|
|
String deptCompanyCode = existingDept != null && existingDept.get("company_code") != null
|
|
? existingDept.get("company_code").toString()
|
|
: null;
|
|
|
|
// 사이클 가드 — 자기 자신/자손을 부모로 지정하려는 시도 차단
|
|
Object newParent = nullIfBlank(bodyParam(body, "parent_dept_code", "parent_dept_code"));
|
|
String newParentCode = newParent != null ? newParent.toString() : null;
|
|
// parent_dept_code cross-tenant / 존재 / 삭제 검증
|
|
if (deptCompanyCode != null) {
|
|
validateParent(newParentCode, deptCompanyCode);
|
|
}
|
|
verifyParentCycle(deptCode, newParentCode);
|
|
|
|
// 부서명 중복 검증 — 본인 dept_code 는 제외
|
|
if (deptCompanyCode != null) {
|
|
Map<String, Object> dupParams = new HashMap<>();
|
|
dupParams.put("company_code", deptCompanyCode);
|
|
dupParams.put("dept_name", deptName);
|
|
Map<String, Object> duplicate = sqlSession.selectOne("department.selectDuplicateDeptName", dupParams);
|
|
if (duplicate != null && !deptCode.equals(duplicate.get("dept_code"))) {
|
|
throw new DuplicateDeptNameException("\"" + deptName + "\" 부서가 이미 존재합니다.");
|
|
}
|
|
}
|
|
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put("dept_code", deptCode);
|
|
params.put("dept_name", deptName);
|
|
params.put("parent_dept_code", newParent);
|
|
params.put("short_name", nullIfBlank(bodyParam(body, "short_name", "short_name")));
|
|
params.put("dept_type", bodyParam(body, "dept_type", "dept_type"));
|
|
params.put("org_system", nullIfBlank(bodyParam(body, "org_system", "org_system")));
|
|
params.put("approval_manager", nullIfBlank(bodyParam(body, "approval_manager", "approval_manager")));
|
|
params.put("dept_manager", nullIfBlank(bodyParam(body, "dept_manager", "dept_manager")));
|
|
params.put("zipcode", nullIfBlank(bodyParam(body, "zipcode", "zipcode")));
|
|
params.put("address1", nullIfBlank(bodyParam(body, "address1", "address1")));
|
|
params.put("address2", nullIfBlank(bodyParam(body, "address2", "address2")));
|
|
params.put("start_date", nullIfBlank(bodyParam(body, "start_date", "start_date")));
|
|
params.put("end_date", nullIfBlank(bodyParam(body, "end_date", "end_date")));
|
|
params.put("sort_order", bodyParam(body, "sort_order", "sort_order"));
|
|
params.put("status", bodyParam(body, "status", "status"));
|
|
// dept_info 추가 필드 (location 코드만 유지 — V019 정리 후)
|
|
params.put("location", nullIfBlank(bodyParam(body, "location", "location")));
|
|
|
|
int updated = sqlSession.update("department.updateDepartment", params);
|
|
if (updated == 0) {
|
|
return null;
|
|
}
|
|
|
|
log.info("부서 수정 성공: deptCode={}", deptCode);
|
|
Map<String, Object> findParams = new HashMap<>();
|
|
findParams.put("dept_code", deptCode);
|
|
return sqlSession.selectOne("department.selectDepartmentByCode", findParams);
|
|
}
|
|
|
|
/**
|
|
* 부서 soft-delete (V1 slim scope).
|
|
* - hard delete 가 아니라 DELETED_AT = NOW() 로 마킹
|
|
* - USER_DEPT 행은 보존 → 복구 시 멤버 그대로 살아남
|
|
* - 활성 자식 부서가 있으면 차단 (deleted 자식은 무시)
|
|
* - 반환: 0 = soft-delete 성공 (보존된 부서원 수는 복구 시점에 재조회)
|
|
* -1 = not found / already deleted
|
|
*/
|
|
@Transactional
|
|
public int deleteDepartment(String deptCode) {
|
|
// 활성 하위 부서 확인 (deleted 자식은 자식 카운트에서 제외)
|
|
Map<String, Object> childParams = new HashMap<>();
|
|
childParams.put("dept_code", deptCode);
|
|
childParams.put("include_deleted", false);
|
|
Number childCountNum = sqlSession.selectOne("department.selectChildDeptCount", childParams);
|
|
int childCount = childCountNum != null ? childCountNum.intValue() : 0;
|
|
if (childCount > 0) {
|
|
throw new IllegalStateException("하위 부서가 있는 부서는 삭제할 수 없습니다. 먼저 하위 부서를 삭제해주세요.");
|
|
}
|
|
|
|
// soft-delete: DELETED_AT = NOW(). USER_DEPT 보존
|
|
Map<String, Object> deptParams = new HashMap<>();
|
|
deptParams.put("dept_code", deptCode);
|
|
int updated = sqlSession.update("department.deleteDepartment", deptParams);
|
|
if (updated == 0) {
|
|
return -1; // not found 또는 이미 deleted
|
|
}
|
|
|
|
log.info("부서 soft-delete 성공: deptCode={} (USER_DEPT 행 보존)", deptCode);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* 부서 복구 (V1 slim scope).
|
|
* - DELETED_AT = NULL 로 되돌림
|
|
* - 부모가 있고 부모도 deleted 상태면 차단 (orphan 방지)
|
|
* - USER_DEPT 행은 soft-delete 시점부터 보존되어왔으므로 자동 복원됨
|
|
*/
|
|
@Transactional
|
|
public RestoreResult restoreDepartment(String deptCode) {
|
|
Map<String, Object> dept = getDepartmentIncludingDeleted(deptCode);
|
|
if (dept == null) {
|
|
return RestoreResult.NOT_FOUND;
|
|
}
|
|
if (dept.get("deleted_at") == null) {
|
|
return RestoreResult.NOT_DELETED;
|
|
}
|
|
|
|
// 부모 deleted 검증
|
|
Object parentObj = dept.get("parent_dept_code");
|
|
if (parentObj != null && !parentObj.toString().isBlank()) {
|
|
String parentCode = parentObj.toString();
|
|
Map<String, Object> parent = getDepartmentIncludingDeleted(parentCode);
|
|
if (parent != null && parent.get("deleted_at") != null) {
|
|
return RestoreResult.PARENT_DELETED;
|
|
}
|
|
}
|
|
|
|
// 동일 이름의 active 부서 중복 검증 (복구 시점)
|
|
Object companyCodeObj = dept.get("company_code");
|
|
Object deptNameObj = dept.get("dept_name");
|
|
if (companyCodeObj != null && deptNameObj != null) {
|
|
Map<String, Object> dupParams = new HashMap<>();
|
|
dupParams.put("company_code", companyCodeObj.toString());
|
|
dupParams.put("dept_name", deptNameObj.toString());
|
|
Map<String, Object> duplicate = sqlSession.selectOne("department.selectDuplicateDeptName", dupParams);
|
|
if (duplicate != null && !deptCode.equals(duplicate.get("dept_code"))) {
|
|
throw new IllegalArgumentException("동일한 이름의 활성 부서가 이미 존재합니다.");
|
|
}
|
|
}
|
|
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put("dept_code", deptCode);
|
|
int restored = sqlSession.update("department.restoreDepartment", params);
|
|
if (restored == 0) {
|
|
return RestoreResult.NOT_DELETED; // race: 동시 복구
|
|
}
|
|
|
|
log.info("부서 복구 성공: deptCode={}", deptCode);
|
|
return RestoreResult.OK;
|
|
}
|
|
|
|
public enum RestoreResult {
|
|
OK, NOT_FOUND, NOT_DELETED, PARENT_DELETED
|
|
}
|
|
|
|
/**
|
|
* parent_dept_code 가 (a) 존재하고 (b) 같은 회사이며 (c) deleted 가 아닌지 검증.
|
|
* null/blank 면 검증 스킵 (최상위 부서).
|
|
*/
|
|
private void validateParent(String parentCode, String companyCode) {
|
|
if (parentCode == null || parentCode.isBlank()) return;
|
|
Map<String, Object> parent = sqlSession.selectOne(
|
|
"department.selectDepartmentByCodeIncludingDeleted",
|
|
Map.of("dept_code", parentCode)
|
|
);
|
|
if (parent == null) {
|
|
throw new IllegalArgumentException("상위 부서를 찾을 수 없습니다: " + parentCode);
|
|
}
|
|
if (parent.get("deleted_at") != null) {
|
|
throw new IllegalArgumentException("삭제된 부서를 상위로 지정할 수 없습니다: " + parentCode);
|
|
}
|
|
Object parentCompany = parent.get("company_code");
|
|
if (parentCompany == null || (!companyCode.equals(parentCompany.toString()) && !"*".equals(parentCompany.toString()))) {
|
|
throw new IllegalArgumentException("다른 회사의 부서를 상위로 지정할 수 없습니다.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* parent_dept_code 변경 시 사이클 검증.
|
|
* deptCode 의 새 부모로 newParent 를 지정하려고 할 때, newParent 또는 그 ancestor
|
|
* 체인에 deptCode 자체가 들어있다면 사이클이 생기므로 차단.
|
|
* (newParent == null 은 최상위로 만들기 — 항상 안전)
|
|
*/
|
|
private void verifyParentCycle(String deptCode, String newParent) {
|
|
if (newParent == null) return;
|
|
if (newParent.equals(deptCode)) {
|
|
throw new IllegalArgumentException("자기 자신을 상위 부서로 지정할 수 없습니다.");
|
|
}
|
|
Set<String> visited = new HashSet<>();
|
|
String cur = newParent;
|
|
while (cur != null && !visited.contains(cur)) {
|
|
if (deptCode.equals(cur)) {
|
|
throw new IllegalArgumentException("선택한 부서는 현재 부서의 하위 부서이므로 상위 부서로 지정할 수 없습니다.");
|
|
}
|
|
visited.add(cur);
|
|
Map<String, Object> p = sqlSession.selectOne(
|
|
"department.selectDepartmentByCodeIncludingDeleted",
|
|
Map.of("dept_code", cur)
|
|
);
|
|
if (p == null) break;
|
|
Object parent = p.get("parent_dept_code");
|
|
cur = parent != null ? parent.toString() : null;
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// 부서원 관리
|
|
// ──────────────────────────────────────────────────
|
|
|
|
public List<Map<String, Object>> getDeptMembers(String deptCode) {
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put("dept_code", deptCode);
|
|
return sqlSession.selectList("department.selectDeptMembers", params);
|
|
}
|
|
|
|
public List<Map<String, Object>> searchUsers(String companyCode, String search) {
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put("company_code", companyCode);
|
|
params.put("search", "%" + search + "%");
|
|
return sqlSession.selectList("department.searchUsers", params);
|
|
}
|
|
|
|
@Transactional
|
|
public void addDeptMember(String deptCode, String userId) {
|
|
// 사용자 존재 확인
|
|
Map<String, Object> userParams = new HashMap<>();
|
|
userParams.put("user_id", userId);
|
|
Map<String, Object> user = sqlSession.selectOne("department.selectUserById", userParams);
|
|
if (user == null) {
|
|
throw new IllegalArgumentException("사용자를 찾을 수 없습니다.");
|
|
}
|
|
|
|
// 이미 부서원인지 확인
|
|
Map<String, Object> existParams = new HashMap<>();
|
|
existParams.put("user_id", userId);
|
|
existParams.put("dept_code", deptCode);
|
|
Map<String, Object> existing = sqlSession.selectOne("department.selectExistingMember", existParams);
|
|
if (existing != null) {
|
|
throw new DuplicateMemberException("이미 해당 부서의 부서원입니다.");
|
|
}
|
|
|
|
// 주 부서가 있는지 확인
|
|
Map<String, Object> primaryParams = new HashMap<>();
|
|
primaryParams.put("user_id", userId);
|
|
Map<String, Object> hasPrimary = sqlSession.selectOne("department.selectUserPrimaryDept", primaryParams);
|
|
|
|
// 부서원 추가
|
|
Map<String, Object> insertParams = new HashMap<>();
|
|
insertParams.put("user_id", userId);
|
|
insertParams.put("dept_code", deptCode);
|
|
insertParams.put("is_primary", hasPrimary == null);
|
|
sqlSession.insert("department.insertDeptMember", insertParams);
|
|
|
|
log.info("부서원 추가 성공: userId={}, deptCode={}", userId, deptCode);
|
|
}
|
|
|
|
@Transactional
|
|
public boolean removeDeptMember(String deptCode, String userId) {
|
|
// 1. 제거 전 — 이 row 가 primary 였는지 확인
|
|
Map<String, Object> existParams = new HashMap<>();
|
|
existParams.put("user_id", userId);
|
|
existParams.put("dept_code", deptCode);
|
|
Map<String, Object> existing = sqlSession.selectOne("department.selectExistingMember", existParams);
|
|
boolean wasPrimary = existing != null && Boolean.TRUE.equals(existing.get("is_primary"));
|
|
|
|
// 2. 제거
|
|
int deleted = sqlSession.delete("department.deleteDeptMember", existParams);
|
|
if (deleted == 0) {
|
|
return false;
|
|
}
|
|
|
|
// 3. primary 였으면 다른 USER_DEPT row 중 하나 promote
|
|
if (wasPrimary) {
|
|
Map<String, Object> remaining = sqlSession.selectOne("department.selectFirstUserDept",
|
|
Map.of("user_id", userId));
|
|
if (remaining != null && remaining.get("dept_code") != null) {
|
|
Map<String, Object> promote = new HashMap<>();
|
|
promote.put("user_id", userId);
|
|
promote.put("dept_code", remaining.get("dept_code").toString());
|
|
sqlSession.update("department.setUserPrimaryDept", promote);
|
|
log.info("주 부서 자동 승격: userId={}, newPrimaryDept={}", userId, remaining.get("dept_code"));
|
|
}
|
|
}
|
|
|
|
log.info("부서원 제거 성공: userId={}, deptCode={}, wasPrimary={}", userId, deptCode, wasPrimary);
|
|
return true;
|
|
}
|
|
|
|
@Transactional
|
|
public void setPrimaryDept(String deptCode, String userId) {
|
|
// 멤버십 검증 — 미소속 부서로 호출 시 데이터 손상 방지
|
|
Map<String, Object> existParams = new HashMap<>();
|
|
existParams.put("user_id", userId);
|
|
existParams.put("dept_code", deptCode);
|
|
Map<String, Object> existing = sqlSession.selectOne("department.selectExistingMember", existParams);
|
|
if (existing == null) {
|
|
throw new IllegalArgumentException("해당 부서의 부서원이 아닙니다. 먼저 부서원으로 추가해주세요.");
|
|
}
|
|
|
|
// 다른 부서의 주 부서 해제
|
|
Map<String, Object> clearParams = new HashMap<>();
|
|
clearParams.put("user_id", userId);
|
|
sqlSession.update("department.clearUserPrimaryDept", clearParams);
|
|
|
|
// 해당 부서를 주 부서로 설정
|
|
Map<String, Object> setParams = new HashMap<>();
|
|
setParams.put("user_id", userId);
|
|
setParams.put("dept_code", deptCode);
|
|
sqlSession.update("department.setUserPrimaryDept", setParams);
|
|
|
|
log.info("주 부서 설정 성공: userId={}, deptCode={}", userId, deptCode);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// 내부 유틸
|
|
// ──────────────────────────────────────────────────
|
|
|
|
private String trimString(Object value) {
|
|
if (value == null) return null;
|
|
String str = value.toString().trim();
|
|
return str.isEmpty() ? null : str;
|
|
}
|
|
|
|
/** snake_case 우선, camelCase fallback으로 request body 파라미터 추출 */
|
|
private Object bodyParam(Map<String, Object> body, String snakeCase, String camelCase) {
|
|
Object val = body.get(snakeCase);
|
|
return val != null ? val : body.get(camelCase);
|
|
}
|
|
|
|
/** 빈 문자열 또는 공백만 있는 문자열을 null 로 치환. 그 외엔 trim 한 값을 반환 */
|
|
private Object nullIfBlank(Object value) {
|
|
if (value == null) return null;
|
|
if (value instanceof String s) {
|
|
String trimmed = s.trim();
|
|
return trimmed.isEmpty() ? null : trimmed;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// ── 중복 예외 클래스 ────────────────────────────────
|
|
|
|
public static class DuplicateDeptNameException extends RuntimeException {
|
|
public DuplicateDeptNameException(String message) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
public static class DuplicateMemberException extends RuntimeException {
|
|
public DuplicateMemberException(String message) {
|
|
super(message);
|
|
}
|
|
}
|
|
}
|