feat(대무자): 대무자 관리 기능 — 결재 위임 + 컨텍스트 주입 + UI #7

Merged
johngreen merged 7 commits from johngreen into main 2026-05-11 23:29:33 +00:00
Contributor

개요

직원 부재 시 다른 직원이 결재를 대신 처리하고, ProfileModal/UserFormModal/결재함 UI 까지 연결하는 대무자(代務者) 관리 기능을 추가합니다.

핵심 결정 (deep-dive Socratic 인터뷰 결과)

항목
대무 범위 결재 대무 (Phase 1) — 일반 메뉴/권한 union 은 Phase 2 descope
권한 기준 원래 사람 A 의 권한 (B 로그인 시 B + A 권한 union)
지정 권한 관리자만 (ADMIN / SUPER_ADMIN)
모드 자동 합쳐서 적용 (B 결재함에 A 결재 자동 노출)
시간 모델 종료일 필수, 시작일 옵션 (비우면 즉시)
다중 대무 N:M 허용, 동일 (A,B) 쌍 활성 기간 겹침은 DB EXCLUDE 제약으로 차단
UI 진입점 UserFormModal "대무자 관리" 섹션 + ProfileModal read-only 조회

주요 변경

백엔드

  • 신규: USER_SUBSTITUTES 테이블 (PK, CHECK 2개, EXCLUDE 제약, 인덱스 2개)
  • 신규: SubstituteService / SubstituteController / mapper/substitute.xml
  • 신규: SubstituteContextFilter/api/** 요청에 effective_user_ids = [user_id, ...active_original_ids] request attribute 주입 (TenantConsistencyGuard 뒤, ForcePasswordChangeGuard 앞)
  • 수정: approval.xml 어댑터 — selectActiveProxyForLineUSER_SUBSTITUTES 참조로 교체 + selectRequests/countRequests/selectMyPendingLines 3곳에 IN (effective_user_ids) foreach 적용
  • 수정: ApprovalService.processApproval 마지막에 AuditLogService.insertAuditLog 호출 추가 (user_id=A, processor_id=B 분리 기록)
  • 수정: auditLog.xmlPROCESSOR_ID/PROCESSOR_NAME 컬럼 추가 + AuditLogService 에서 평시 fallback + 대무 시 USER_INFO.USER_NAME 단건 lookup
  • 수정: StartupSchemaMigrator 에 RUN_086 7개 idempotent DDL/DML 등록 — backend 부팅 시 메타 + 모든 활성 테넌트 DB 에 자동 적용

프론트엔드

  • 신규: frontend/lib/api/substitute.ts (7개 API)
  • 신규: components/admin/SubstituteSection.tsx — 관리자 대무자 지정 (v5 토큰, blur 금지)
  • 신규: components/layout/MySubstituteView.tsx — ProfileModal read-only 조회 (양방향, D-day 카운트다운)
  • 수정: UserFormModal / ProfileModal / approval/page.tsx (대무 뱃지)

인프라

  • DB-per-tenant 멀티테넌시는 TenantConsistencyGuardFilter 가 자동 차단 (추가 코드 0줄)
  • DB 마이그레이션은 StartupSchemaMigrator 가 자동 적용 — db/migrations/RUN_086_MIGRATION.md 는 runbook 문서

적용된 critic 권장사항

  • B1: AuditLogService 에서 대무 이벤트 한정 USER_INFO lookup
  • B2: APPROVAL_PROXY_SETTINGS read path 어댑터 + RUN_086 데이터 복사
  • B3: cross-company 검증 (validateUserInCompany)
  • B4: 결재 대무만 Phase 1, 메뉴 union 은 Phase 2 (notes/johngreen scan 문서 첨부)
  • B5: btree_gist hard prerequisite, trigger fallback 삭제
  • DB-level CHECK (self-위임 차단), SUPER_ADMIN 을 proxy 로 지정 거부
  • ensureEffectiveUserIds helper (mybatis 빈 IN() 방어)
  • SubstituteSection / MySubstituteView 별도 컴포넌트 분리

검증 결과

  • 백엔드 compileJava 통과
  • 프론트엔드 tsc --noEmit 통과
  • StartupSchemaMigrator — meta + 3 tenants (test01/test02/siflex) 모두 OK
  • backend 컨테이너 startup 정상 (mybatis XML 파싱 통과)

산출 문서

  • .omc/specs/deep-dive-trace-user-substitute-management.md — trace 분석
  • .omc/specs/deep-dive-user-substitute-management.md — 최종 spec
  • .omc/plans/autopilot-impl.md — architect plan + critic 검증
  • notes/johngreen/2026-05-11-domain-tables-created-by-scan.md — T15 broad scan

남은 일

  • 수동 QA: 관리자 로그인 → UserFormModal 대무자 지정 → B 로그인 → 결재함 뱃지 확인
  • Phase 2: 메뉴/AUTHORITY_MASTER 룩업에 IN (effective_user_ids) 적용 (별도 PR)
## 개요 직원 부재 시 다른 직원이 결재를 대신 처리하고, ProfileModal/UserFormModal/결재함 UI 까지 연결하는 대무자(代務者) 관리 기능을 추가합니다. ## 핵심 결정 (deep-dive Socratic 인터뷰 결과) | 항목 | 값 | |---|---| | 대무 범위 | 결재 대무 (Phase 1) — 일반 메뉴/권한 union 은 Phase 2 descope | | 권한 기준 | 원래 사람 A 의 권한 (B 로그인 시 B + A 권한 union) | | 지정 권한 | 관리자만 (ADMIN / SUPER_ADMIN) | | 모드 | 자동 합쳐서 적용 (B 결재함에 A 결재 자동 노출) | | 시간 모델 | 종료일 필수, 시작일 옵션 (비우면 즉시) | | 다중 대무 | N:M 허용, 동일 (A,B) 쌍 활성 기간 겹침은 DB EXCLUDE 제약으로 차단 | | UI 진입점 | UserFormModal "대무자 관리" 섹션 + ProfileModal read-only 조회 | ## 주요 변경 ### 백엔드 - **신규**: `USER_SUBSTITUTES` 테이블 (PK, CHECK 2개, EXCLUDE 제약, 인덱스 2개) - **신규**: `SubstituteService` / `SubstituteController` / `mapper/substitute.xml` - **신규**: `SubstituteContextFilter` — `/api/**` 요청에 `effective_user_ids = [user_id, ...active_original_ids]` request attribute 주입 (TenantConsistencyGuard 뒤, ForcePasswordChangeGuard 앞) - **수정**: `approval.xml` 어댑터 — `selectActiveProxyForLine` 를 `USER_SUBSTITUTES` 참조로 교체 + `selectRequests`/`countRequests`/`selectMyPendingLines` 3곳에 `IN (effective_user_ids)` foreach 적용 - **수정**: `ApprovalService.processApproval` 마지막에 `AuditLogService.insertAuditLog` 호출 추가 (`user_id=A`, `processor_id=B` 분리 기록) - **수정**: `auditLog.xml` 에 `PROCESSOR_ID`/`PROCESSOR_NAME` 컬럼 추가 + `AuditLogService` 에서 평시 fallback + 대무 시 `USER_INFO.USER_NAME` 단건 lookup - **수정**: `StartupSchemaMigrator` 에 RUN_086 7개 idempotent DDL/DML 등록 — backend 부팅 시 메타 + 모든 활성 테넌트 DB 에 자동 적용 ### 프론트엔드 - **신규**: `frontend/lib/api/substitute.ts` (7개 API) - **신규**: `components/admin/SubstituteSection.tsx` — 관리자 대무자 지정 (v5 토큰, blur 금지) - **신규**: `components/layout/MySubstituteView.tsx` — ProfileModal read-only 조회 (양방향, D-day 카운트다운) - **수정**: `UserFormModal` / `ProfileModal` / `approval/page.tsx` (대무 뱃지) ### 인프라 - DB-per-tenant 멀티테넌시는 `TenantConsistencyGuardFilter` 가 자동 차단 (추가 코드 0줄) - DB 마이그레이션은 `StartupSchemaMigrator` 가 자동 적용 — `db/migrations/RUN_086_MIGRATION.md` 는 runbook 문서 ## 적용된 critic 권장사항 - B1: AuditLogService 에서 대무 이벤트 한정 USER_INFO lookup - B2: APPROVAL_PROXY_SETTINGS read path 어댑터 + RUN_086 데이터 복사 - B3: cross-company 검증 (`validateUserInCompany`) - B4: 결재 대무만 Phase 1, 메뉴 union 은 Phase 2 (notes/johngreen scan 문서 첨부) - B5: btree_gist hard prerequisite, trigger fallback 삭제 - DB-level CHECK (self-위임 차단), SUPER_ADMIN 을 proxy 로 지정 거부 - `ensureEffectiveUserIds` helper (mybatis 빈 IN() 방어) - SubstituteSection / MySubstituteView 별도 컴포넌트 분리 ## 검증 결과 - 백엔드 `compileJava` 통과 - 프론트엔드 `tsc --noEmit` 통과 - StartupSchemaMigrator — meta + 3 tenants (test01/test02/siflex) **모두 OK** - backend 컨테이너 startup 정상 (mybatis XML 파싱 통과) ## 산출 문서 - `.omc/specs/deep-dive-trace-user-substitute-management.md` — trace 분석 - `.omc/specs/deep-dive-user-substitute-management.md` — 최종 spec - `.omc/plans/autopilot-impl.md` — architect plan + critic 검증 - `notes/johngreen/2026-05-11-domain-tables-created-by-scan.md` — T15 broad scan ## 남은 일 - 수동 QA: 관리자 로그인 → UserFormModal 대무자 지정 → B 로그인 → 결재함 뱃지 확인 - Phase 2: 메뉴/AUTHORITY_MASTER 룩업에 `IN (effective_user_ids)` 적용 (별도 PR)
johngreen added 7 commits 2026-05-11 23:23:26 +00:00
- USER_SUBSTITUTES 테이블 (PK, CHECK 2개, EXCLUDE 제약, 인덱스 2개)
- btree_gist 확장 hard prerequisite
- SYSTEM_AUDIT_LOG.PROCESSOR_ID/PROCESSOR_NAME 컬럼 추가
- APPROVAL_PROXY_SETTINGS → USER_SUBSTITUTES idempotent 데이터 복사
- 사전/사후 검증 SQL + 롤백 섹션 포함
- substitute.xml (namespace=substitute): selectSubstituteList/Cnt, selectMySubstitutes,
  selectActiveOriginalUserIds (Filter 핫패스), selectActiveProxyForLine (결재 어댑터),
  countOverlap, countUserInCompany, countSuperAdmin, insert/update/delete
- SubstituteService extends BaseService: 관리자 권한 검증, self-위임 차단,
  cross-company 검증, SUPER_ADMIN 을 proxy 로 지정 거부, 사전 겹침 검증
- SubstituteController: /api/substitutes CRUD + /mine (read-only) + /check-overlap
- TenantConsistencyGuard 뒤, ForcePasswordChangeGuard 앞에 등록
- /api/** 요청에 effective_user_ids = [user_id, ...active_original_ids] attribute 세팅
- SUPER_ADMIN (company_code='*') 은 short-circuit
- DB 조회 실패 시 본 요청 차단 안 함 (가용성 우선, warn 로그)
- approval.xml selectActiveProxyForLine: APPROVAL_PROXY_SETTINGS → USER_SUBSTITUTES 참조
  (BOOLEAN IS_ACTIVE, START_DATE NULL 허용)
- approval.xml 3곳 (selectRequests/countRequests/selectMyPendingLines):
  APPROVER_ID = #{user_id} → APPROVER_ID IN (effective_user_ids) foreach
- ApprovalService.ensureEffectiveUserIds helper — mybatis 빈 IN() 방지
- ApprovalController: getRequests/getMyPendingLines 에 @RequestAttribute(effective_user_ids) 추가
- ApprovalService.processApproval 마지막에 AuditLogService.insertAuditLog 호출 추가
  (user_id=A, processor_id=B 분리 기록)
- auditLog.xml insertAuditLog INSERT 절에 PROCESSOR_ID/PROCESSOR_NAME 컬럼 추가
- auditLog.xml selectUserNameById — 처리자 이름 lookup
- AuditLogService.insertAuditLog:
  · processor_id null → user_id 로 자동 채움 (평시 = 동일)
  · processor_id != user_id 이고 processor_name null → USER_INFO 단건 조회 (대무 이벤트만)
- notes: 도메인 테이블 CREATED_BY/UPDATED_BY broad scan — actual processor(B) 통일 자동 만족
- frontend/lib/api/substitute.ts: 7개 API 함수 (Record<string, any> 컨벤션)
- components/admin/SubstituteSection.tsx (신규): 관리자용 대무자 지정 섹션
  · 활성/예정 대무 관계 테이블, 사전 겹침 검증
  · v5 토큰 (--v5-surface-solid, --v5-glow-sm) 사용, blur 금지
- components/admin/UserFormModal.tsx: 수정 모드일 때 SubstituteSection 노출
- components/layout/MySubstituteView.tsx (신규): ProfileModal 용 read-only 조회
  · 내 대무자 + 내가 대무 중인 사람 양방향, D-day 카운트다운
- components/layout/ProfileModal.tsx: MySubstituteView 삽입
- app/(main)/approval/page.tsx: 대기함 행에 "대무 ← {원본 결재자}" 뱃지
  · currentUser.user_id !== line.approver_id 비교 (별도 타입 필드 X)
invyone 의 정상 마이그레이션 패턴(StartupSchemaMigrator)에 RUN_086 의 7개 idempotent
DDL/DML 등록. 다음 backend 부팅 시 메타 DB + 모든 활성 테넌트 DB 에 자동 적용됨.

- btree_gist 확장 (CREATE EXTENSION IF NOT EXISTS)
- USER_SUBSTITUTES 테이블 (PK, CHECK 2, EXCLUDE 제약, 인덱스 2)
- SYSTEM_AUDIT_LOG ALTER (PROCESSOR_ID/PROCESSOR_NAME ADD COLUMN IF NOT EXISTS)
- APPROVAL_PROXY_SETTINGS → USER_SUBSTITUTES idempotent 데이터 복사

호환성 보정:
- EXCLUDE 의 daterange lower 에 COALESCE(.., CURRENT_DATE) 사용 불가 (IMMUTABLE 만 허용)
  → daterange(START_DATE, END_DATE, '[]') 단순 형식. NULL lower = -infinity 자연 처리.
- APPROVAL_PROXY_SETTINGS 의 START_DATE/END_DATE 가 varchar → DATE cast 추가
- 원본 메타데이터(created/updated) 컬럼명 환경별 차이 회피 → 'migration_086' + NOW() 고정

실측: meta+3 tenants 모두 OK (test01/test02/siflex)
johngreen merged commit 6561aad7ef into main 2026-05-11 23:29:33 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gbpark/invyone#7