feat: integrate dashboard modules and document history

This commit is contained in:
hyunho
2026-03-26 18:03:07 +09:00
parent baf6019c1c
commit 61b5638cb1
22 changed files with 14252 additions and 79 deletions

359
docs/AUTH_DB_DESIGN.md Normal file
View File

@@ -0,0 +1,359 @@
# Auth DB Design
## Goal
현재 조직도 업무 데이터와 로그인 데이터를 분리한다.
분리 원칙:
- 업무 데이터는 기존 `public.members`, `seat_maps`, `seat_positions` 중심으로 유지
- 인증/권한 데이터는 별도 `auth` 스키마로 분리
- 로그인 사용자는 필요할 때만 `members.id` 와 연결
이 방식이면 비밀번호, 세션, 감사로그를 업무 데이터와 분리해서 관리할 수 있고,
엑셀 임포트로 `members` 가 갱신돼도 인증 체계가 직접 흔들리지 않는다.
## Scope
이번 설계의 대상:
- 사용자 계정
- 비밀번호 해시
- 세션
- 역할과 권한
- 로그인 감사로그
- 사용자와 조직 구성원 연결
이번 설계에서 제외:
- SSO 연동
- OAuth/OpenID Connect
- MFA
- 비밀번호 재설정 메일 발송
## Recommended Structure
권장 구조는 "같은 PostgreSQL, 다른 스키마" 이다.
- 업무 스키마: `public`
- 인증 스키마: `auth`
초기 운영에서는 DB 인스턴스를 분리하지 않아도 된다.
대신 아래 원칙은 바로 적용한다.
- 애플리케이션 계정도 가능하면 읽기/쓰기 범위를 분리
- 인증 관련 쿼리는 `auth.*` 만 접근
- 업무 API 는 `public.*` 중심으로 접근
## Core Tables
### `auth.users`
로그인 가능한 계정의 기준 테이블.
주요 컬럼:
- `id BIGSERIAL PRIMARY KEY`
- `username TEXT NOT NULL UNIQUE`
- `password_hash TEXT NOT NULL`
- `display_name TEXT NOT NULL`
- `email TEXT`
- `status TEXT NOT NULL DEFAULT 'active'`
- `member_id INTEGER NULL REFERENCES public.members(id) ON DELETE SET NULL`
- `last_login_at TIMESTAMPTZ`
- `password_changed_at TIMESTAMPTZ`
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
- `updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
상태값 권장:
- `active`
- `locked`
- `disabled`
원칙:
- `username` 는 로그인 식별자
- `member_id` 는 선택 연결
- 구성원이 퇴사하거나 엑셀에서 빠져도 계정 자체는 바로 삭제하지 않음
### `auth.roles`
권한 묶음 정의.
주요 컬럼:
- `id BIGSERIAL PRIMARY KEY`
- `code TEXT NOT NULL UNIQUE`
- `name TEXT NOT NULL`
- `description TEXT`
초기 권장 역할:
- `super_admin`
- `org_admin`
- `viewer`
### `auth.user_roles`
사용자와 역할의 다대다 연결.
주요 컬럼:
- `user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE`
- `role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE`
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
- `PRIMARY KEY (user_id, role_id)`
### `auth.permissions`
세분화된 권한 코드 정의.
주요 컬럼:
- `id BIGSERIAL PRIMARY KEY`
- `code TEXT NOT NULL UNIQUE`
- `name TEXT NOT NULL`
- `description TEXT`
초기 권장 권한:
- `member.read`
- `member.write`
- `member.import`
- `seatmap.read`
- `seatmap.write`
- `photo.upload`
- `admin.user.manage`
### `auth.role_permissions`
역할과 권한의 다대다 연결.
주요 컬럼:
- `role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE`
- `permission_id BIGINT NOT NULL REFERENCES auth.permissions(id) ON DELETE CASCADE`
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
- `PRIMARY KEY (role_id, permission_id)`
### `auth.sessions`
서버 세션 저장 테이블.
주요 컬럼:
- `id UUID PRIMARY KEY`
- `user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE`
- `refresh_token_hash TEXT`
- `issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
- `expires_at TIMESTAMPTZ NOT NULL`
- `revoked_at TIMESTAMPTZ`
- `ip_address INET`
- `user_agent TEXT`
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
원칙:
- 브라우저 쿠키에는 세션 식별자만 저장
- 토큰 자체를 평문으로 DB에 저장하지 않음
- 만료와 강제 로그아웃을 DB에서 통제 가능하게 함
### `auth.login_audit_logs`
로그인 시도와 결과 기록.
주요 컬럼:
- `id BIGSERIAL PRIMARY KEY`
- `username TEXT NOT NULL`
- `user_id BIGINT NULL REFERENCES auth.users(id) ON DELETE SET NULL`
- `success BOOLEAN NOT NULL`
- `failure_reason TEXT`
- `ip_address INET`
- `user_agent TEXT`
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`
용도:
- 로그인 실패 추적
- 계정 잠금 기준 판단
- 보안 감사 대응
## Relationship To Current `members`
핵심은 `auth.users.member_id -> public.members.id` 연결이다.
의미:
- 로그인 계정과 조직도 인원을 분리한다
- 로그인하지 않는 구성원은 `members` 에만 있어도 된다
- 외부 관리자 계정은 `member_id` 없이 운영할 수 있다
권장 규칙:
- 일반 사내 사용자는 `employee_id` 기준으로 계정-구성원 연결
- 엑셀 동기화 시 `members.id` 유지가 중요하므로 이미 반영한 비교 기반 동기화 방식을 유지
- `member_id` 연결이 끊긴 계정은 자동 삭제하지 말고 관리자 검토 대상으로 둔다
## Login Flow
### 1. 로그인 요청
입력:
- `username`
- `password`
처리:
- `auth.users` 에서 `username` 조회
- `status != active` 이면 거부
- `password_hash` 검증
- 성공 시 `auth.sessions` 생성
- `auth.login_audit_logs` 기록
응답 권장:
- 사용자 기본 정보
- 역할 목록
- 권한 목록
- 세션 만료 시각
### 2. 인증 확인
각 보호 API 요청 시:
- 세션 쿠키 또는 Bearer 토큰 확인
- `auth.sessions` 조회
- 만료/폐기 여부 확인
- 사용자 상태와 역할 재검증
### 3. 로그아웃
처리:
- 현재 세션의 `revoked_at` 업데이트
- 클라이언트 쿠키 제거
## Authorization Model
초기에는 RBAC 기반으로 충분하다.
권장 역할별 범위:
`super_admin`
- 사용자 관리
- 권한 관리
- 조직도/사진/자리배치 전체 수정
`org_admin`
- 조직도 조회/수정
- 엑셀 임포트
- 사진 업로드
- 자리배치 수정
`viewer`
- 조직도 조회
- 자리배치 조회
API 보호 예시:
- `GET /api/members`: `member.read`
- `POST /api/members/import`: `member.import`
- `POST /api/uploads/profile-photo`: `photo.upload`
- `PUT /api/seat-maps/{seat_map_id}/layout`: `seatmap.write`
- 사용자 관리 API: `admin.user.manage`
## Password Policy
비밀번호는 평문 저장 금지.
권장:
- `Argon2id` 우선
- 대안으로 `bcrypt`
추가 원칙:
- 첫 구현부터 해시 알고리즘 버전 정보 포함
- 비밀번호 변경 시 `password_changed_at` 갱신
- 실패 횟수 기반 잠금은 앱 로직 또는 별도 컬럼으로 확장 가능
## Migration Plan
### Phase 1
인증 스키마와 기본 테이블만 추가.
작업:
- `CREATE SCHEMA IF NOT EXISTS auth`
- `auth.users`
- `auth.roles`
- `auth.user_roles`
- `auth.permissions`
- `auth.role_permissions`
- `auth.sessions`
- `auth.login_audit_logs`
이 단계에서는 기존 `/api/mock-login` 유지 가능.
### Phase 2
관리자 1명 이상을 수동 생성하고 실제 로그인 API 추가.
권장 추가 API:
- `POST /api/auth/login`
- `POST /api/auth/logout`
- `GET /api/auth/me`
### Phase 3
기존 프론트엔드의 mock 로그인 제거.
변경 대상:
- [frontend/public/app.js](/home/hyunho/projects/mh-dashboard-organization/frontend/public/app.js)
- [backend/app/main.py](/home/hyunho/projects/mh-dashboard-organization/backend/app/main.py)
- [backend/app/config.py](/home/hyunho/projects/mh-dashboard-organization/backend/app/config.py)
### Phase 4
권한 기반으로 API 보호 적용.
우선순위:
1. 쓰기 API 보호
2. 업로드 API 보호
3. 읽기 API 권한 정리
## Recommended SQL Skeleton
```sql
CREATE SCHEMA IF NOT EXISTS auth;
CREATE TABLE IF NOT EXISTS auth.users (
id BIGSERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
display_name TEXT NOT NULL,
email TEXT,
status TEXT NOT NULL DEFAULT 'active',
member_id INTEGER NULL REFERENCES public.members(id) ON DELETE SET NULL,
last_login_at TIMESTAMPTZ,
password_changed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS auth.roles (
id BIGSERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT
);
CREATE TABLE IF NOT EXISTS auth.user_roles (
user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE IF NOT EXISTS auth.sessions (
id UUID PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
refresh_token_hash TEXT,
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
## Operational Notes
- 엑셀 임포트는 계속 `public.members` 기준으로 처리
- 로그인 계정 생성은 엑셀 업로드와 분리
- 프로필 사진은 현재처럼 파일 저장 + `members.photo_url` 참조 유지 가능
- 감사로그와 세션은 삭제보다 보존 기간 정책으로 관리
## Decision
현재 프로젝트의 권장안은 아래 한 줄로 정리된다.
"로그인 DB는 `auth` 스키마로 분리하고, 업무 DB는 `public` 에 유지하며, 두 영역은 `auth.users.member_id` 로만 연결한다."