9.1 KiB
9.1 KiB
History / As-Of DB Plan
Goal
월간 스냅샷 파일을 따로 만드는 대신, DB 자체를 시간축이 있는 구조로 전환한다. 목표는 다음과 같다.
- 조직도와 자리배치도를 수정할 때마다 과거 값이 사라지지 않게 누적 저장
- 사용자가 특정 날짜 또는 기간을 선택하면 그 시점 기준 상태를 다시 조회
- 날짜가 원래 없는 데이터도
유효 시작일과유효 종료일을 부여해 과거 버전 조회 가능하게 만들기
핵심 원칙은 아래 한 줄이다.
- 최신 값을 덮어쓰지 않고,
valid_from,valid_to기반 버전 행을 누적한다
Why This Instead Of Snapshots
- 월간 스냅샷 파일은 생성 시점만 남고 중간 변경 추적이 약하다
- 원하는 날짜 기준으로 바로 조회하기 어렵다
- 조직도만 따로 파일로 남으면 자리배치도, 권한, 운영 이력을 함께 맞추기 어렵다
따라서 이 프로젝트에는 "파일 스냅샷"보다 "시점 조회 가능한 버전 DB"가 더 맞다.
Query Model
조회 기준은 as_of 또는 date_from, date_to 이다.
- 특정 날짜 조회:
GET /api/members?as_of=2026-03-01GET /api/seat-maps/active/layout?as_of=2026-03-01
- 기간 비교:
GET /api/history/organization/compare?date_from=2026-03-01&date_to=2026-03-31
공통 조회 조건은 아래다.
WHERE valid_from <= :as_of
AND (valid_to IS NULL OR valid_to > :as_of)
Recommended Data Model
1. Stable Base Tables
식별자와 최소 메타만 유지하는 기준 테이블.
CREATE TABLE members (
id SERIAL PRIMARY KEY,
employee_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE seat_assignment_targets (
member_id INTEGER PRIMARY KEY REFERENCES members(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
설명:
members는 "사람 자체" 식별자 역할- 실제 이름, 조직, 직급, 연락처, 좌석 같은 표시 데이터는 버전 테이블로 이동
2. Member Version Table
CREATE TABLE member_versions (
id BIGSERIAL PRIMARY KEY,
member_id INTEGER NOT NULL REFERENCES members(id) ON DELETE CASCADE,
name TEXT NOT NULL,
company TEXT NOT NULL DEFAULT '',
rank TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT '',
department TEXT NOT NULL DEFAULT '',
grp TEXT NOT NULL DEFAULT '',
division TEXT NOT NULL DEFAULT '',
team TEXT NOT NULL DEFAULT '',
cell TEXT NOT NULL DEFAULT '',
work_status TEXT NOT NULL DEFAULT '',
work_time TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '',
photo_url TEXT NOT NULL DEFAULT '',
valid_from TIMESTAMPTZ NOT NULL,
valid_to TIMESTAMPTZ,
revision_no BIGINT NOT NULL,
changed_by_user_id BIGINT,
change_reason TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX member_versions_member_time_idx
ON member_versions (member_id, valid_from, valid_to);
설명:
- 날짜가 원래 없던 조직도 데이터도 이 테이블에서 과거 버전 관리
- 어떤 시점에 이름, 조직, 직책, 연락처가 어땠는지 재구성 가능
3. Seat Assignment Version Table
CREATE TABLE seat_assignment_versions (
id BIGSERIAL PRIMARY KEY,
member_id INTEGER NOT NULL REFERENCES members(id) ON DELETE CASCADE,
seat_map_id INTEGER REFERENCES seat_maps(id) ON DELETE CASCADE,
seat_slot_id INTEGER REFERENCES seat_slots(id) ON DELETE CASCADE,
seat_label TEXT NOT NULL DEFAULT '',
valid_from TIMESTAMPTZ NOT NULL,
valid_to TIMESTAMPTZ,
revision_no BIGINT NOT NULL,
changed_by_user_id BIGINT,
change_reason TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX seat_assignment_versions_member_time_idx
ON seat_assignment_versions (member_id, valid_from, valid_to);
설명:
- 현재
seat_positions가 맡는 "최신 좌석 상태"를 버전형으로 저장 - 특정 날짜의 자리배치도를 다시 그릴 수 있음
4. Optional Change Event Table
CREATE TABLE entity_change_events (
id BIGSERIAL PRIMARY KEY,
entity_type TEXT NOT NULL,
entity_id BIGINT NOT NULL,
action_type TEXT NOT NULL,
revision_no BIGINT NOT NULL,
changed_by_user_id BIGINT,
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
change_reason TEXT NOT NULL DEFAULT '',
patch_json JSONB NOT NULL DEFAULT '{}'::jsonb
);
설명:
- 버전 테이블은 "그 시점의 전체 값"
- 이벤트 테이블은 "무엇이 바뀌었는지"
- 초기에는 없어도 되지만, 추후 비교 UI와 감사로그에 유용
5. Revision Table
CREATE TABLE history_revisions (
id BIGSERIAL PRIMARY KEY,
scope TEXT NOT NULL DEFAULT 'organization',
revision_label TEXT NOT NULL,
created_by_user_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
note TEXT NOT NULL DEFAULT ''
);
설명:
- 버전 묶음을 사람 친화적으로 관리할 때 사용
- 예:
2026-03-27 1차 조직개편 반영
How Writes Change
현재 구조:
UPDATE members SET ...UPSERT seat_positions ...
바꿀 구조:
- 현재 유효한 버전 행을 조회
- 값이 달라지면 기존 행의
valid_to를 닫음 - 새 값을 가진 행을
valid_from = now()로 insert - 필요하면 최신 캐시 테이블도 함께 갱신
예시:
UPDATE member_versions
SET valid_to = NOW()
WHERE member_id = :member_id
AND valid_to IS NULL;
INSERT INTO member_versions (
member_id, name, company, rank, role, department, grp, division, team, cell,
work_status, work_time, phone, email, photo_url,
valid_from, valid_to, revision_no, changed_by_user_id, change_reason
)
VALUES (
:member_id, :name, :company, :rank, :role, :department, :grp, :division, :team, :cell,
:work_status, :work_time, :phone, :email, :photo_url,
NOW(), NULL, :revision_no, :changed_by_user_id, :change_reason
);
How Date-Bearing And Date-Less Data Coexist
날짜가 원래 있는 데이터
integration_work_logs.work_dateintegration_vouchers.issue_date
이 데이터는 원래 날짜 컬럼이 있으므로 그대로 사용하면 된다.
날짜가 원래 없는 데이터
- 조직도 인원 기본 정보
- 조직 소속
- 자리배치 상태
- 사진 경로
이 데이터는 valid_from, valid_to 를 붙여 시점 조회가 가능하게 만든다.
즉, "날짜가 없는 데이터"가 아니라 "유효기간을 부여한 버전 데이터"로 바꾸는 것이다.
API Direction
Common UI Input
사용자가 실제 HTML에서 고르는 기준은 헤더의 날짜 제어를 공통 입력으로 쓰는 것이 맞다.
권장안:
- 프로젝트/팀 분석: 기존처럼
시작일 ~ 종료일 - 조직도/자리배치도: 우선
기준일(as_of)1개를 사용 - 필요하면 조직도 비교 화면에서
비교 시작일,비교 종료일확장
현재 상태:
- 헤더 날짜 제어는
프로젝트별 분석,팀/개인별 분석iframe에 이미 전달되고 있음 - 조직도/자리배치도는 아직 헤더 날짜를 실제 조회 조건으로 사용하지 않음
권장 API:
GET /api/members?as_of=2026-03-27
GET /api/members/{id}?as_of=2026-03-27
GET /api/seat-maps/active/layout?as_of=2026-03-27
GET /api/history/organization/compare?date_from=2026-03-01&date_to=2026-03-31
Migration Strategy
Phase 1. History Tables Add
member_versionsseat_assignment_versionshistory_revisions- 필요 시
entity_change_events
현재 members, seat_positions 는 그대로 유지
Phase 2. Backfill
- 현재
members최신값을member_versions(valid_from = NOW(), valid_to = NULL)로 적재 - 현재
seat_positions최신값을seat_assignment_versions(valid_from = NOW(), valid_to = NULL)로 적재 - 이 단계에서는 과거 진짜 이력은 없고 "현재 상태를 버전 구조에 싣는 것"이 목표
Phase 3. Dual Write
- 조직도 수정 시:
- 기존
members갱신 - 동시에
member_versions에 append
- 기존
- 자리배치 저장 시:
- 기존
seat_positions갱신 - 동시에
seat_assignment_versions에 append
- 기존
Phase 4. As-Of Read APIs
- 조직도 API에
as_of지원 - 자리배치도 API에
as_of지원 - 헤더 날짜 제어와 연결
Phase 5. Full History-First Read
- 최신 조회도 버전 테이블 기준으로 전환
members,seat_positions는 캐시 또는 편의 테이블로 축소 가능
Recommended First Scope
처음부터 모든 테이블을 이력화하지 말고 아래부터 시작하는 것이 안전하다.
members->member_versionsseat_positions->seat_assignment_versions- 조직도/자리배치도 조회 API에
as_of
이 세 가지가 되면 사용자는 원하는 날짜의 조직 상태와 좌석 상태를 볼 수 있다.
Explicitly Removed From Scope
- 월간 스냅샷 파일 생성
- 스냅샷 다운로드 기능
- 조직도만 따로 파일로 내보내는 방식
이 프로젝트의 방향은 "파일 스냅샷"이 아니라 "시점 조회 가능한 버전 DB"다.