4개 도메인 병렬 분석 결과 + 통합 요약 + 시나리오 중심 정리.
총 25개 버그 (CRITICAL 3 / HIGH 11 / MEDIUM 7 / LOW 4) 식별.
실제 수정은 별도 커밋 (commit 68c1cb5b).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
15 KiB
부서관리 페이지 버그 정리 — 시나리오 중심
작성일: 2026-05-08 목적: 사용자/개발자가 "어떤 일이 벌어지는지" 바로 이해할 수 있게 정리
🔴 진짜 위험한 것 — 즉시 수정
1. 다른 회사 사용자 정보가 그냥 보입니다 🚨
어디서: DepartmentController.java:234 searchUsers
무슨 일이 벌어지나:
- 부서원 추가 시 사용자 검색 API 가 있음
- 그런데 이 API 는 "당신이 어느 회사 사람인지" 검사를 안 함
- 즉, A 회사 직원이 URL 에 B 회사 코드만 박으면 → B 회사 직원 명단을 그대로 받아옴
왜 심각한가:
- 멀티테넌시 (회사간 격리) 의 핵심 원칙이 깨짐
- 다른 모든 부서 API 는 회사 격리 검사를 하는데 이거 하나만 빠짐
- 또한 일반 USER 도 admin 검사 없이 호출 가능
고치는 법:
- 컨트롤러에
if (!isSuperAdmin(...) && !userCompanyCode.equals(companyCode)) return 403;추가 - admin role 체크도 추가
2. 사용자의 "주 부서" 가 통째로 사라질 수 있습니다 🚨
어디서: DepartmentService.java:356-370 setPrimaryDept
무슨 일이 벌어지나:
- 김철수 씨가 영업부 소속 (영업부 = 주 부서)
- 누군가 "김철수를 마케팅부의 주 부서로 설정" API 를 호출 (그런데 김철수는 마케팅부 소속이 아님)
- 코드는 "일단 김철수의 모든 주 부서 표시 해제" 부터 실행 → 영업부 주 부서 표시 사라짐
- 그 다음 "마케팅부를 주 부서로 설정" 시도 → 김철수가 마케팅부 소속이 아니므로 0 row 변경 (조용히 실패)
- 결과: 김철수는 어디에도 주 부서가 없는 사용자가 됨
왜 심각한가:
- 데이터 무결성 (모든 사용자는 주 부서 1개) 이 깨진 채 commit
- 에러도 안 남
- 결재 흐름 등에서 주 부서 기준 로직이 망가짐
고치는 법:
- API 진입 시
selectExistingMember(userId, deptCode)로 멤버십 검증 - 멤버 아니면 400 반환
setUserPrimaryDept가 0 row affected 면 예외 던져서 트랜잭션 롤백
3. 다른 회사의 부서를 부모로 지정할 수 있습니다 🚨
어디서: DepartmentService.java:107 (생성), :251 verifyParentCycle (수정)
무슨 일이 벌어지나:
- 부서 생성/수정 시
parent_dept_code를 받음 - 이 코드가 (a) 실제 존재하는지, (b) 같은 회사인지, (c) 삭제된 건 아닌지 전혀 검사 안 함
- 사이클 검사 (
verifyParentCycle) 는 있지만 "부모가 같은 회사인지" 검증은 빠져있음
왜 심각한가:
- SUPER_ADMIN 또는 컨트롤러 우회 경로로 다른 회사 부서를 부모로 지정 가능
- 회사간 데이터가 트리로 엮여 멀티테넌시 침해
고치는 법:
- create/update 양쪽에서 parent 부서 조회 → company_code 일치 검증 + deleted_at IS NULL 검증
🟠 사용자가 자주 마주칠 버그
4. 회사를 바꿔도 우측 상세 패널에 이전 회사 부서가 그대로 남습니다
어디서: page.tsx:658 회사 Select 의 onValueChange
시나리오:
- 회사 A 의 "영업부" 클릭 → 우측에 영업부 정보
- 좌측 상단에서 회사 B 로 변경
- 좌측 트리는 회사 B 부서로 갱신됨
- 그런데 우측 상세 패널은 여전히 영업부 (회사 A) 정보 표시
- 사용자가 모르고 저장 버튼 누르면 회사 코드 mismatch 로 이상한 일이 벌어짐
고치는 법:
- 회사 변경 시
handleClearDetail()호출하여 상세 패널 초기화
5. 부서 정렬 변경 (한 칸 위/아래) 이 부분 실패할 수 있습니다
어디서: page.tsx:403 handleMove
시나리오:
- 형제 부서 5개 정렬 변경 시 코드는 5번의 PUT 요청을 동시에 보냄 (
Promise.all) - 그 중 1개가 실패하면 → 4개는 이미 DB 에 반영됨
- 코드는 toast 만 띄우고 화면 새로고침은 안 함 (catch 블록에
loadDepartments()없음) - 화면은 이전 정렬, DB 는 부분 업데이트된 상태로 영구 불일치
고치는 법:
- catch 블록에서도
loadDepartments()호출하여 DB 상태로 동기화 - 더 좋은 방법: 백엔드에 "정렬 일괄 업데이트" API 만들어 트랜잭션 보장
6. 검색하면 트리가 망가집니다
어디서: page.tsx:231-246 filteredDepts + childrenOf
시나리오:
- "인사" 검색 → "인사팀" 부서가 매칭됨
- 그런데 인사팀의 부모인 "경영지원본부" 는 매칭 안 됨
childrenOf(null)은 매칭된 루트만 보여주는데 인사팀의 부모가 없으니 트리에 안 나타남- 결과: 검색 결과가 있어도 트리에 아무것도 안 보일 수 있음
고치는 법:
- 매칭된 노드의 부모 체인을 자동으로 결과에 포함
- 또는 검색 모드에서는 트리가 아닌 평면 리스트로 표시
7. update 가 삭제된 부서도 변경합니다 (silent corruption)
어디서: department.xml:160-179 updateDepartment
무슨 일이 벌어지나:
- 사용자가 휴지통의 "옛 부서" 를 수정하려 시도
- 컨트롤러는
getDepartmentIncludingDeleted로 검증 후 service 호출 - SQL UPDATE 는
WHERE DEPT_CODE = ?만 있어 삭제 여부 무관하게 update 실행 - service 가 다음 줄에서
selectDepartmentByCode(DELETED_AT IS NULL) 로 재조회 → null - controller 가 404 반환
결과: 사용자는 "404 부서를 찾을 수 없음" 메시지를 보지만 DB 는 이미 update 된 상태. 데이터 손상.
고치는 법:
- SQL UPDATE WHERE 절에
AND DELETED_AT IS NULL추가 - 0 row 면 service 가 null 반환 → 404 일관됨
8. 부서명을 바꿀 때 중복 검사를 안 합니다
어디서: DepartmentService.java:131-170 updateDepartment
시나리오:
- 새 부서 생성 시에는 "이미 같은 이름 있어요" 체크함
- 그런데 부서명 수정 시에는 체크 없음
- 결과: 기존 "영업1팀" 을 "영업팀" 으로 수정 → 이미 있는 "영업팀" 과 동일 이름 두 부서 공존 가능
같은 문제 — 복구 시에도: restoreDepartment 에서도 동일. 삭제했던 부서를 살릴 때 같은 이름의 active 부서가 있어도 충돌 검사 없음.
고치는 법:
- update/restore 양쪽에
selectDuplicateDeptName호출 추가 - 더 좋은 방법: DB 에 partial UNIQUE 인덱스 (
(company_code, lower(trim(dept_name))) WHERE deleted_at IS NULL) 추가
9. 일반 회사 관리자가 글로벌 부서를 삭제할 수 있습니다
어디서: DepartmentController.java:135 deleteDepartment
시나리오:
- 시스템 공통 부서 (company_code='*') 가 있음 (예: 시스템 관리자, 공통 부서 등)
- 어느 회사 admin 이든 이 글로벌 부서의 dept_code 를 알면 DELETE 호출 가능
- 코드의 권한 검사:
isAdmin(role)통과 +canAccessDept(글로벌이라 통과) → 삭제 진행
고치는 법:
- 글로벌 부서 (
company_code = '*') 의 write 작업은isSuperAdmin만 허용
10. 사용자가 입력한 부서코드가 조용히 무시됩니다 -> 사용자가 부서코드를 입력하게 하지말고 시스템이 동적으로 생성할수있게변경 입력값은 받지않게 수정
어디서: DepartmentService.java:85-99 createDepartment
시나리오:
- 사용자가 부서코드란에 "SALES-A" 입력 (대시 포함)
- 백엔드 정규식은
^[A-Za-z0-9_]+$만 허용 → 불일치 - 코드는 예외를 던지지 않고 자동으로
DEPT_47같은 코드 생성 - 응답은 201 성공, 하지만
dept_code가DEPT_47(사용자가 입력한 값과 다름) - 사용자는 "SALES-A" 로 만들어졌다고 착각
고치는 법:
- 정규식 불일치 시 400 + "부서 코드는 영문/숫자/언더스코어만 가능합니다" 메시지
11. 새 부서의 "시작일" 이 항상 오늘로 강제 저장됩니다
어디서: page.tsx:113 emptyDraft, page.tsx:957 일괄등록
시나리오:
- UI 에 시작일/종료일 입력란은 V2 로 미뤄져 hidden 처리됨 (
{false && ...}) - 그런데
emptyDraft는start_date: new Date().toISOString().slice(0,10)으로 항상 today 입력 - 결과: 사용자는 시작일 입력란을 본 적도 없는데 모든 신규 부서에 today 가 강제 저장됨
- 일괄등록도 동일 — 100개 import 하면 100개 모두 import 한 날짜가 시작일
고치는 법:
emptyDraft의start_date기본값을 빈 문자열로 변경- 또는 컬럼이 hidden 인 상태에서는 payload 에 포함하지 않음
🟡 알아두면 좋은 문제
12. 부서원 목록이 다른 부서 것으로 잠깐 보일 수 있습니다
어디서: page.tsx:219-228 멤버 fetch useEffect
상황: 네트워크 느린 환경에서 부서 A 클릭 → 멤버 로딩 중 → 부서 B 클릭 → A 의 응답이 늦게 도착하여 B 의 패널에 A 의 멤버 표시. fetch 취소 로직 (AbortController) 없음.
고치는 법: useEffect 에 cancellation flag 추가
13. 삭제 다이얼로그를 열어둔 채 다른 부서를 선택하면 잘못된 부서가 삭제됩니다
어디서: page.tsx:503 handleDelete
상황:
- 영업부 선택 → 메인 패널의 "삭제" 버튼 클릭 → 다이얼로그 열림
- 다이얼로그 뒤로 트리에서 다른 부서 (예: 인사부) 클릭 →
selectedCode= 인사부 - 다이얼로그의 "삭제" 확정 → 코드는 최신
selectedCode사용 → 인사부가 삭제됨
shadcn Dialog 는 보통 포커스 트랩이 있지만 dismiss 가능한 설정이면 위험.
고치는 법: 다이얼로그 열 때 dept_code 를 캡처해서 클로저로 전달
14. X 버튼 누르면 미저장 변경이 경고 없이 사라집니다
어디서: page.tsx:299 handleClearDetail
상황: 부서 정보 수정 중 우측 상단 X 버튼 클릭 → 즉시 폼 초기화. isDirty 상태는 계산되지만 사용 안 함.
고치는 법: if (isDirty && !confirm("변경사항이 있습니다. 폐기하시겠습니까?")) return;
15. 변경이력 기능이 화면만 있고 실제로는 동작 안 합니다
어디서: page.tsx:997-1027
상황: "변경이력" 버튼 클릭 → 다이얼로그 열림 → 항상 "데이터를 불러오는 중이거나, 등록된 이력이 없습니다." 표시. 실제 API 호출 코드 없음.
고치는 법: 백엔드 audit log API 와 연동. 기능 미구현 명시.
16. 부서원 추가/제거 UI 가 없습니다
어디서: page.tsx:1469 MembersPanel
상황:
- 부서원 정보 탭에서 멤버 목록만 표시
- 추가/제거 버튼 없음
- 백엔드 API (
addDepartmentMember,removeDepartmentMember,searchUsers) 는 존재하지만 UI 미연결
고치는 법: 멤버 추가 모달 + 행별 제거 버튼 구현
17. 일괄등록 실패 사유가 안 보입니다
어디서: page.tsx:957-985
상황: 100개 import → "성공 95건 / 실패 5건" 토스트만. 어떤 부서코드가 왜 실패했는지 알 수 없어 재시도 불가.
고치는 법: 실패 항목을 모달이나 다운로드 가능한 결과 화면으로 표시
🟢 사소한 문제 (정리/보강)
18. 동시에 부서 생성하면 실패할 수 있음
어디서: DepartmentService.java:96 selectNextDeptNumber
MAX(번호) + 1 이 비원자적이라 두 사람이 동시에 부서 생성 시 같은 번호 → 두 번째 INSERT 가 PK 위반으로 5xx 에러. catch 로 잡지도 않아 사용자에게 raw 500 노출.
19. 부서 코드/이름의 앞뒤 공백이 그대로 저장됨
어디서: DepartmentService.java:107+ nullIfBlank
nullIfBlank 는 "완전히 빈 문자열만 null 로" 처리. " DEPT_3 " 같은 공백 포함 코드는 그대로 저장됨. → nullIfBlank 자체를 trim+blank→null 한 번에 처리하도록 수정.
20. 검색 후 "전체 펼치기" 누르면 검색 해제 시 부분 펼침 상태로 남음
어디서: page.tsx:249 expandAll
filteredDepts 만 펼치므로 검색 결과만 expanded. 검색 해제 후 트리가 일부만 펼쳐진 상태로 보임.
21. 컨트롤러에 dead code 있음
어디서: DepartmentController.java:271-272
Object userIdObj = body.get("user_id");
if (userIdObj == null) userIdObj = body.get("user_id"); // 같은 키 두 번
camelCase fallback 의도였는데 오타로 같은 snake_case. → body.get("userId") 로 수정.
22. Picker 에 "최상위로 이동" 옵션이 UI 에 없음
어디서: page.tsx:920 DepartmentPicker.onSelect
handleConfirmMoveTo(null) 코드 경로는 있지만 picker UI 에서 "부모 없음 (최상위)" 선택지가 없어 트리거 불가.
23. SQL 의 LIKE 와일드카드 미이스케이프
어디서: DepartmentService.java:283 searchUsers
% + 사용자입력 + %. 사용자가 _ 만 입력하면 모든 사용자 매칭. 데이터 누출은 아니지만 enumeration 가능.
24. dead SQL 쿼리 잔존
어디서: department.xml:192-195 deleteUserDeptByDeptCode
주석에 "Slice 2.1 이후 사용 안 함" 명시되어 있지만 쿼리 그대로 남아있음.
25. 부서원 목록 INNER JOIN
어디서: department.xml:213 selectDeptMembers
USER_INFO 에서 사용자가 삭제되면 USER_DEPT row 가 남아있어도 멤버에 안 나타남. member_count 와 실제 표시되는 멤버 수가 불일치 가능.
📋 수정 권장 순서
Phase 1 — 보안/데이터 손상 (최우선)
- #1 searchUsers 회사/role 가드
- #2 setPrimaryDept 멤버십 검증
- #3 parent_dept_code 회사 격리 검증
- #9 글로벌 부서 write 권한 강화
Phase 2 — 데이터 무결성
- #7 update SQL 의 DELETED_AT 누락 수정
- #8 update/restore 부서명 중복 검증
- #4 회사 변경 시 상세 패널 초기화
- #5 handleMove 실패 처리
Phase 3 — 사용자 경험
- #6 검색 트리 broken 수정
- #10 dept_code silent fallback 제거
- #11 start_date 강제 저장 제거
- #12 ~ #14 race/실수 방지
Phase 4 — 기능 완성
- #15 변경이력 API 연동
- #16 부서원 추가/제거 UI
- #17 일괄등록 실패 상세
Phase 5 — 정리
- #18 ~ #25 race/공백/dead code/검색 정밀화
도메인별 상세 리포트
- 프론트엔드 React:
2026-05-08-부서관리-버그헌팅-frontend.md - 백엔드 Java:
2026-05-08-부서관리-버그헌팅-backend.md - SQL/Mapper:
2026-05-08-부서관리-버그헌팅-sql.md - UX 시나리오:
2026-05-08-부서관리-버그헌팅-ux.md