-- ===================================================== -- 부산교통공사 1호선 고장코드 시스템 - 데이터베이스 스키마 -- ===================================================== -- 1. 부서 테이블 (departments) -- 소속 부서 관리 및 권한 제어를 위한 테이블 CREATE TABLE IF NOT EXISTS public.departments ( id SERIAL PRIMARY KEY, code VARCHAR(20) UNIQUE NOT NULL, -- 부서 코드 (예: SPC001, NPC001) name VARCHAR(100) NOT NULL, -- 부서명 (예: 신평차량, 노포차량, 차량처) description TEXT, -- 부서 설명 is_active BOOLEAN DEFAULT true, -- 활성화 여부 created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- 부서 코드에 인덱스 생성 (검색 성능 향상) CREATE INDEX IF NOT EXISTS idx_departments_code ON public.departments(code); CREATE INDEX IF NOT EXISTS idx_departments_is_active ON public.departments(is_active); -- 부서 테이블에 초기 데이터 삽입 INSERT INTO public.departments (code, name, description) VALUES ('SPC', '신평차량', '신평차량사업소'), ('NPC', '노포차량', '노포차량사업소'), ('VHD', '차량처', '차량처') ON CONFLICT (code) DO NOTHING; -- 2. 사용자 테이블 (users) -- 회원 정보 및 인증 정보를 저장하는 테이블 CREATE TABLE IF NOT EXISTS public.users ( id SERIAL PRIMARY KEY, auth_id UUID UNIQUE, -- Supabase Auth의 사용자 ID (외래키 개념) email VARCHAR(255) UNIQUE NOT NULL, -- 이메일 (humetro.busan.kr 도메인만) employee_id VARCHAR(50) UNIQUE NOT NULL, -- 사번 name VARCHAR(100) NOT NULL, -- 이름 department_id INTEGER NOT NULL REFERENCES public.departments(id) ON DELETE RESTRICT, -- 소속 부서 is_active BOOLEAN DEFAULT true, -- 계정 활성화 여부 last_login_at TIMESTAMPTZ, -- 마지막 로그인 시각 created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- 사용자 테이블 인덱스 CREATE INDEX IF NOT EXISTS idx_users_auth_id ON public.users(auth_id); CREATE INDEX IF NOT EXISTS idx_users_email ON public.users(email); CREATE INDEX IF NOT EXISTS idx_users_employee_id ON public.users(employee_id); CREATE INDEX IF NOT EXISTS idx_users_department_id ON public.users(department_id); CREATE INDEX IF NOT EXISTS idx_users_is_active ON public.users(is_active); -- 3. 부서별 데이터 접근 권한 테이블 (department_permissions) -- 향후 부서별 고장코드 조회/수정 권한 관리를 위한 테이블 CREATE TABLE IF NOT EXISTS public.department_permissions ( id SERIAL PRIMARY KEY, department_id INTEGER NOT NULL REFERENCES public.departments(id) ON DELETE CASCADE, resource_type VARCHAR(50) NOT NULL, -- 'fault_code', 'signal', 'mmi_code' 등 can_read BOOLEAN DEFAULT true, -- 조회 권한 can_write BOOLEAN DEFAULT false, -- 수정 권한 can_delete BOOLEAN DEFAULT false, -- 삭제 권한 created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(department_id, resource_type) ); -- 권한 테이블 인덱스 CREATE INDEX IF NOT EXISTS idx_dept_permissions_dept_id ON public.department_permissions(department_id); -- 부서별 기본 권한 설정 (모든 부서에 조회 권한 부여) INSERT INTO public.department_permissions (department_id, resource_type, can_read, can_write, can_delete) SELECT d.id, 'fault_code', true, false, false FROM public.departments d ON CONFLICT (department_id, resource_type) DO NOTHING; INSERT INTO public.department_permissions (department_id, resource_type, can_read, can_write, can_delete) SELECT d.id, 'signal', true, false, false FROM public.departments d ON CONFLICT (department_id, resource_type) DO NOTHING; INSERT INTO public.department_permissions (department_id, resource_type, can_read, can_write, can_delete) SELECT d.id, 'mmi_code', true, false, false FROM public.departments d ON CONFLICT (department_id, resource_type) DO NOTHING; -- 4. 생체인증 credential 테이블 (biometric_credentials) -- WebAuthn/생체인증을 위한 credential 저장 CREATE TABLE IF NOT EXISTS public.biometric_credentials ( id SERIAL PRIMARY KEY, employee_id VARCHAR(50) NOT NULL REFERENCES public.users(employee_id) ON DELETE CASCADE, credential_id VARCHAR(255) UNIQUE NOT NULL, -- WebAuthn credential ID credential_data JSONB NOT NULL, -- Credential 전체 데이터 created_at TIMESTAMPTZ DEFAULT NOW(), last_used_at TIMESTAMPTZ, UNIQUE(employee_id, credential_id) ); -- 생체인증 인덱스 CREATE INDEX IF NOT EXISTS idx_biometric_employee_id ON public.biometric_credentials(employee_id); CREATE INDEX IF NOT EXISTS idx_biometric_credential_id ON public.biometric_credentials(credential_id); -- 5. 감사 로그 테이블 (audit_logs) - 선택사항 -- 사용자의 중요한 작업을 기록하기 위한 테이블 CREATE TABLE IF NOT EXISTS public.audit_logs ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL, action VARCHAR(50) NOT NULL, -- 'login', 'logout', 'create', 'update', 'delete' resource_type VARCHAR(50), -- 'fault_code', 'user', 'department' 등 resource_id VARCHAR(100), -- 대상 리소스의 ID details JSONB, -- 추가 상세 정보 ip_address VARCHAR(45), -- 사용자 IP 주소 created_at TIMESTAMPTZ DEFAULT NOW() ); -- 감사 로그 인덱스 CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON public.audit_logs(user_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON public.audit_logs(action); CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON public.audit_logs(created_at); -- 5. 트리거: updated_at 자동 업데이트 CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- departments 테이블에 트리거 적용 DROP TRIGGER IF EXISTS update_departments_updated_at ON public.departments; CREATE TRIGGER update_departments_updated_at BEFORE UPDATE ON public.departments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- users 테이블에 트리거 적용 DROP TRIGGER IF EXISTS update_users_updated_at ON public.users; CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- department_permissions 테이블에 트리거 적용 DROP TRIGGER IF EXISTS update_dept_permissions_updated_at ON public.department_permissions; CREATE TRIGGER update_dept_permissions_updated_at BEFORE UPDATE ON public.department_permissions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- 6. Row Level Security (RLS) 정책 - 선택사항 -- Supabase에서 RLS를 활성화하면 부서별 데이터 접근을 자동으로 제어할 수 있습니다 -- RLS 활성화 ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; ALTER TABLE public.departments ENABLE ROW LEVEL SECURITY; ALTER TABLE public.department_permissions ENABLE ROW LEVEL SECURITY; -- 부서 테이블: 모든 인증된 사용자가 조회 가능 CREATE POLICY "부서 목록은 모든 사용자가 조회 가능" ON public.departments FOR SELECT TO authenticated USING (is_active = true); -- 사용자 테이블: 자기 자신의 정보만 조회 가능 CREATE POLICY "사용자는 본인 정보만 조회 가능" ON public.users FOR SELECT TO authenticated USING (auth.uid() = auth_id); -- 사용자 테이블: 자기 자신의 정보만 업데이트 가능 (특정 필드만) CREATE POLICY "사용자는 본인 정보만 수정 가능" ON public.users FOR UPDATE TO authenticated USING (auth.uid() = auth_id) WITH CHECK (auth.uid() = auth_id); -- 7. 뷰 생성: 사용자 정보와 부서 정보를 조인한 뷰 CREATE OR REPLACE VIEW public.users_with_department AS SELECT u.id, u.auth_id, u.email, u.employee_id, u.name, u.department_id, d.code as department_code, d.name as department_name, u.is_active, u.last_login_at, u.created_at, u.updated_at FROM public.users u LEFT JOIN public.departments d ON u.department_id = d.id; -- ===================================================== -- 사용 예시 및 참고사항 -- ===================================================== -- 1. 새로운 부서 추가 -- INSERT INTO public.departments (code, name, description) -- VALUES ('NEW001', '새로운부서', '부서 설명'); -- 2. 사용자 조회 (부서 정보 포함) -- SELECT * FROM public.users_with_department WHERE email = 'user@humetro.busan.kr'; -- 3. 부서별 사용자 수 조회 -- SELECT d.name, COUNT(u.id) as user_count -- FROM public.departments d -- LEFT JOIN public.users u ON d.id = u.department_id -- GROUP BY d.id, d.name -- ORDER BY user_count DESC; -- 4. 특정 부서의 권한 조회 -- SELECT d.name, dp.resource_type, dp.can_read, dp.can_write, dp.can_delete -- FROM public.department_permissions dp -- JOIN public.departments d ON dp.department_id = d.id -- WHERE d.code = 'SPC'; -- ===================================================== -- 마이그레이션 롤백 (필요시) -- ===================================================== -- DROP VIEW IF EXISTS public.users_with_department; -- DROP TABLE IF EXISTS public.audit_logs CASCADE; -- DROP TABLE IF EXISTS public.department_permissions CASCADE; -- DROP TABLE IF EXISTS public.users CASCADE; -- DROP TABLE IF EXISTS public.departments CASCADE; -- DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE;