회사 관리 기능 확장 + 테넌트/비번 보안 하드닝

- 첫 로그인 비번 강제 변경 (RUN_082): FORCE_PASSWORD_CHANGE 컬럼,
  ForcePasswordChangeGuardFilter, /auth/change-password API + 페이지
- 테넌트 일관성 가드: TenantConsistencyGuardFilter 로 JWT.company_code
  ↔ 서브도메인 company_code 대조, CompanyResolver 가 (db_name, company_code)
  동시 반환
- 회사 관리 확장 (RUN_083 audit log, RUN_084 lifecycle 컬럼):
  CompanyAdmin/Members/Templates/Lifecycle/AuditLog 서비스 +
  CompanyMgmtController + SuperAdminGuard
- 회사 관리 UI: CompanyAccordionRow 탭화 + 모달 4종
  (AdminInfo/Deactivate/Delete/RecopyTemplates) + AuditLogDrawer + csvExport
- 프로비저닝 마법사: force_password_change 토글 반영
- 프론트 인증: storage 이벤트 멀티탭 동기화, 403 errorCode
  (PASSWORD_CHANGE_REQUIRED / CROSS_TENANT_REJECTED / TENANT_NOT_RESOLVED)
  전역 리다이렉트
- 기타: StartupSchemaMigrator, OS별 도커 기동 스크립트, CLAUDE.md 트래킹

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 00:36:05 +09:00
parent 06998cd2a5
commit 68f85f3736
58 changed files with 6110 additions and 316 deletions
+70
View File
@@ -0,0 +1,70 @@
# 082 마이그레이션 — 첫 로그인 비밀번호 강제 변경 컬럼
작성일: 2026-04-24
작성자: gbpark
관련: 회사 프로비저닝 `FORCE_PW_CHANGE` 토글 실제 동작 구현
## 목적
Step 3 마법사의 "첫 로그인 시 비밀번호 변경 강제" 토글이 실제로 동작하도록,
`USER_INFO` 테이블에 플래그 컬럼을 추가한다.
- 프로비저닝 시 관리자 계정 생성 시점에 플래그 true 로 INSERT
- 로그인 시 플래그 true 면 프론트가 `/change-password` 로 강제 이동
- 비밀번호 변경 성공 시 플래그 false 로 클리어
## 추가 컬럼
| 컬럼 | 타입 | 기본값 | 용도 |
|---|---|---|---|
| `FORCE_PASSWORD_CHANGE` | BOOLEAN | `FALSE` | true 면 로그인 성공 직후 비밀번호 변경 강제. 변경 완료 시 false 로 갱신 |
> `PASSWORD_CHANGED_AT` 는 일단 추가하지 않음 — 필요해지면 후속 마이그레이션.
## SQL
```sql
-- 082: 첫 로그인 비밀번호 강제 변경 플래그
ALTER TABLE USER_INFO
ADD COLUMN IF NOT EXISTS FORCE_PASSWORD_CHANGE BOOLEAN DEFAULT FALSE;
```
## 실행
### 1) 메타 DB (= 프로비저닝 스키마 원본)
신규 회사 DB 는 `SchemaCopier` 가 메타 DB 를 `pg_dump --schema-only` 로 복제하므로,
**메타 DB 에만 컬럼 추가하면 이후 신규 회사는 자동 반영**된다.
```bash
psql -h 183.99.177.40 -U postgres -d invyone <<'SQL'
ALTER TABLE USER_INFO
ADD COLUMN IF NOT EXISTS FORCE_PASSWORD_CHANGE BOOLEAN DEFAULT FALSE;
SQL
```
### 2) 기존 회사 DB 들
이미 프로비저닝된 회사 DB 에도 동일 ALTER 필요. DB 이름 목록은 메타 DB 의
`COMPANY_MNG.DB_NAME` 에서 확인:
```bash
psql -h 183.99.177.40 -U postgres -d invyone -c \
"SELECT DB_NAME FROM COMPANY_MNG WHERE DB_STATUS='active';"
```
각 DB 에 대해:
```bash
for DB in $(psql -h 183.99.177.40 -U postgres -d invyone -t -c \
"SELECT DB_NAME FROM COMPANY_MNG WHERE DB_STATUS='active'"); do
psql -h 183.99.177.40 -U postgres -d "$DB" -c \
"ALTER TABLE USER_INFO ADD COLUMN IF NOT EXISTS FORCE_PASSWORD_CHANGE BOOLEAN DEFAULT FALSE;"
done
```
## 롤백
```sql
ALTER TABLE USER_INFO DROP COLUMN IF EXISTS FORCE_PASSWORD_CHANGE;
```
+81
View File
@@ -0,0 +1,81 @@
# 083 마이그레이션 — 회사 관리 감사 로그 테이블
작성일: 2026-04-24
작성자: gbpark
관련: 회사 관리 화면 (`/admin/sysMng/subdomainList`) 라이프사이클 액션 감사 로그
## 목적
회사 관리 화면에서 수행되는 SUPER_ADMIN 레벨 액션을 메타 DB 에 추적 가능하게 기록.
### 기록 대상 액션
| action | 기록 지점 |
|---|---|
| `COMPANY_CREATE` | 프로비저닝 성공 시 (FINALIZE) |
| `COMPANY_CREATE_FAILED` | 프로비저닝 실패 시 (자동 롤백 후) |
| `COMPANY_DEACTIVATE` | `DB_STATUS=suspended` 변경 시 |
| `COMPANY_REACTIVATE` | `suspended → active` 복귀 시 |
| `COMPANY_DELETE` | 영구 삭제 (DROP DATABASE + row DELETE) 시 |
| `ADMIN_PASSWORD_RESET` | 관리자 계정 비번 재설정 시 |
| `TEMPLATES_RECOPY` | 템플릿 재복제 시 |
## 추가 테이블
```sql
CREATE TABLE IF NOT EXISTS COMPANY_AUDIT_LOG (
ID BIGSERIAL PRIMARY KEY,
COMPANY_CODE VARCHAR(30) NOT NULL,
ACTOR_USER_ID VARCHAR(50), -- 수행자 (JWT 에서 뽑아낸 user_id). 개발모드는 NULL 가능
ACTION VARCHAR(40) NOT NULL, -- 위 표의 action 값
TARGET VARCHAR(100), -- 영향받은 대상 (db_name, admin user_id 등)
DETAILS JSONB, -- 추가 컨텍스트 (reason, before/after 값 등)
SUCCESS BOOLEAN DEFAULT TRUE,
ERROR_MESSAGE TEXT, -- 실패 시 에러 메시지
CREATED_AT TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS IDX_AUDIT_COMPANY_CODE
ON COMPANY_AUDIT_LOG (COMPANY_CODE, CREATED_AT DESC);
CREATE INDEX IF NOT EXISTS IDX_AUDIT_CREATED
ON COMPANY_AUDIT_LOG (CREATED_AT DESC);
CREATE INDEX IF NOT EXISTS IDX_AUDIT_ACTION
ON COMPANY_AUDIT_LOG (ACTION, CREATED_AT DESC);
```
## 실행 (메타 DB 만)
이 테이블은 **메타 DB (`invyone`) 에만** 존재해야 한다. 테넌트 DB 는 복제 대상 아님.
`SchemaCopier` 는 메타 DB 를 `pg_dump --schema-only` 로 복제하므로, 새 테넌트 DB 에 이 테이블이
딸려가는 걸 막으려면 이 테이블은 `COMPANY_MNG` 와 같이 "메타 전용" 취급.
### SchemaCopier 의 `--exclude-table` 처리 확인 필요
현재 구현은 public 스키마 전체를 복제. `COMPANY_AUDIT_LOG` 가 테넌트 DB 에도 생성되지만
**어차피 쓰지 않으므로 빈 채로 남는다.** 용량 문제 없음 → MVP 로는 허용. 추후 정리.
```bash
psql -h 183.99.177.40 -U postgres -d invyone <<'SQL'
CREATE TABLE IF NOT EXISTS COMPANY_AUDIT_LOG (
ID BIGSERIAL PRIMARY KEY,
COMPANY_CODE VARCHAR(30) NOT NULL,
ACTOR_USER_ID VARCHAR(50),
ACTION VARCHAR(40) NOT NULL,
TARGET VARCHAR(100),
DETAILS JSONB,
SUCCESS BOOLEAN DEFAULT TRUE,
ERROR_MESSAGE TEXT,
CREATED_AT TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS IDX_AUDIT_COMPANY_CODE ON COMPANY_AUDIT_LOG (COMPANY_CODE, CREATED_AT DESC);
CREATE INDEX IF NOT EXISTS IDX_AUDIT_CREATED ON COMPANY_AUDIT_LOG (CREATED_AT DESC);
CREATE INDEX IF NOT EXISTS IDX_AUDIT_ACTION ON COMPANY_AUDIT_LOG (ACTION, CREATED_AT DESC);
SQL
```
## 롤백
```sql
DROP TABLE IF EXISTS COMPANY_AUDIT_LOG;
```
+64
View File
@@ -0,0 +1,64 @@
# 084 마이그레이션 — COMPANY_MNG 라이프사이클 컬럼 추가
작성일: 2026-04-24
작성자: gbpark
관련: 회사 관리 화면 비활성화/템플릿 트래킹 기능
## 목적
회사 관리 화면의 "비활성화 + 템플릿 탭 + 라이프사이클 이벤트" 지원을 위해
`COMPANY_MNG` 에 메타 필드 3 개 추가.
## 추가 컬럼
| 컬럼 | 타입 | 기본값 | 용도 |
|---|---|---|---|
| `INSTALLED_GROUPS` | `JSONB` | `'[]'::jsonb` | 프로비저닝 시 설치된 `TableGroup` id 배열 (예: `["SCREEN","CONTROL","BATCH","DASHBOARD"]`). 템플릿 탭 렌더에 사용 |
| `DEACTIVATED_AT` | `TIMESTAMP` | `NULL` | `DB_STATUS='suspended'` 가 된 시각. 재활성화 시 NULL 로 리셋 |
| `DEACTIVATION_REASON` | `VARCHAR(200)` | `NULL` | SUPER_ADMIN 이 입력한 비활성화 사유 (감사 로그와 별개로 COMPANY_MNG 에도 즉시 조회용) |
## SQL
```sql
-- 084: 회사 라이프사이클 메타 컬럼
ALTER TABLE COMPANY_MNG
ADD COLUMN IF NOT EXISTS INSTALLED_GROUPS JSONB DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS DEACTIVATED_AT TIMESTAMP,
ADD COLUMN IF NOT EXISTS DEACTIVATION_REASON VARCHAR(200);
```
## 실행 (메타 DB 만)
```bash
psql -h 183.99.177.40 -U postgres -d invyone <<'SQL'
ALTER TABLE COMPANY_MNG
ADD COLUMN IF NOT EXISTS INSTALLED_GROUPS JSONB DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS DEACTIVATED_AT TIMESTAMP,
ADD COLUMN IF NOT EXISTS DEACTIVATION_REASON VARCHAR(200);
SQL
```
## 기존 회사 row 백필 (선택)
기존에 이미 프로비저닝된 회사 row 는 `INSTALLED_GROUPS` 가 빈 배열로 남음.
프로비저닝 로직이 지금은 선택 그룹 정보를 버리므로 소급 복원 불가 → 빈 배열 유지.
템플릿 탭은 "정보 없음" 으로 표시.
정확한 값이 필요하면 `TableGroup.values()` 의 required=true 3개(SCREEN, CONTROL, BATCH) 는
모든 회사에 필수로 들어갔으므로 최소 백필 가능:
```sql
UPDATE COMPANY_MNG
SET INSTALLED_GROUPS = '["SCREEN","CONTROL","BATCH"]'::jsonb
WHERE INSTALLED_GROUPS = '[]'::jsonb
AND DB_STATUS = 'active';
```
## 롤백
```sql
ALTER TABLE COMPANY_MNG
DROP COLUMN IF EXISTS INSTALLED_GROUPS,
DROP COLUMN IF EXISTS DEACTIVATED_AT,
DROP COLUMN IF EXISTS DEACTIVATION_REASON;
```