-- 근로 및 휴가 관리를 위한 테이블 확장 -- 작성일: 2025-11-03 -- 1. 근로 유형 테이블 생성 CREATE TABLE IF NOT EXISTS work_attendance_types ( id INT PRIMARY KEY AUTO_INCREMENT, type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '근로 유형 코드', type_name VARCHAR(50) NOT NULL COMMENT '근로 유형명', description TEXT COMMENT '설명', is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT='근로 유형 관리 테이블'; -- 2. 휴가 유형 테이블 생성 CREATE TABLE IF NOT EXISTS vacation_types ( id INT PRIMARY KEY AUTO_INCREMENT, type_code VARCHAR(20) NOT NULL UNIQUE COMMENT '휴가 유형 코드', type_name VARCHAR(50) NOT NULL COMMENT '휴가 유형명', hours_deduction DECIMAL(4,2) NOT NULL COMMENT '차감 시간', description TEXT COMMENT '설명', is_active BOOLEAN DEFAULT TRUE COMMENT '활성 상태', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT='휴가 유형 관리 테이블'; -- 3. 일일 근태 기록 테이블 생성 CREATE TABLE IF NOT EXISTS daily_attendance_records ( id INT PRIMARY KEY AUTO_INCREMENT, record_date DATE NOT NULL COMMENT '기록 날짜', worker_id INT NOT NULL COMMENT '작업자 ID', total_work_hours DECIMAL(4,2) DEFAULT 0 COMMENT '총 작업 시간', attendance_type_id INT COMMENT '근로 유형 ID', vacation_type_id INT NULL COMMENT '휴가 유형 ID', is_vacation_processed BOOLEAN DEFAULT FALSE COMMENT '휴가 처리 여부', overtime_approved BOOLEAN DEFAULT FALSE COMMENT '초과근무 승인 여부', overtime_approved_by INT NULL COMMENT '초과근무 승인자 ID', overtime_approved_at TIMESTAMP NULL COMMENT '초과근무 승인 시간', status ENUM('incomplete', 'partial', 'complete', 'overtime', 'vacation', 'error') DEFAULT 'incomplete' COMMENT '상태', notes TEXT COMMENT '비고', created_by INT NOT NULL COMMENT '생성자 ID', updated_by INT NULL COMMENT '수정자 ID', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 외래키 제약조건 FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE, FOREIGN KEY (attendance_type_id) REFERENCES work_attendance_types(id), FOREIGN KEY (vacation_type_id) REFERENCES vacation_types(id), FOREIGN KEY (overtime_approved_by) REFERENCES users(user_id), FOREIGN KEY (created_by) REFERENCES users(user_id), FOREIGN KEY (updated_by) REFERENCES users(user_id), -- 유니크 제약조건 (작업자별 날짜별 하나의 기록) UNIQUE KEY unique_worker_date (worker_id, record_date), -- 인덱스 INDEX idx_record_date (record_date), INDEX idx_worker_date (worker_id, record_date), INDEX idx_status (status) ) COMMENT='일일 근태 기록 테이블'; -- 4. 휴가 잔여 관리 테이블 생성 CREATE TABLE IF NOT EXISTS worker_vacation_balance ( id INT PRIMARY KEY AUTO_INCREMENT, worker_id INT NOT NULL COMMENT '작업자 ID', year YEAR NOT NULL COMMENT '연도', total_annual_leave DECIMAL(4,2) DEFAULT 15.0 COMMENT '연간 총 연차 (일)', used_annual_leave DECIMAL(4,2) DEFAULT 0 COMMENT '사용한 연차 (일)', remaining_annual_leave DECIMAL(4,2) GENERATED ALWAYS AS (total_annual_leave - used_annual_leave) STORED COMMENT '잔여 연차 (일)', notes TEXT COMMENT '비고', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 외래키 제약조건 FOREIGN KEY (worker_id) REFERENCES workers(worker_id) ON DELETE CASCADE, -- 유니크 제약조건 (작업자별 연도별 하나의 기록) UNIQUE KEY unique_worker_year (worker_id, year), -- 인덱스 INDEX idx_worker_year (worker_id, year) ) COMMENT='작업자별 휴가 잔여 관리 테이블'; -- 5. 기본 데이터 삽입 -- 근로 유형 기본 데이터 INSERT INTO work_attendance_types (type_code, type_name, description) VALUES ('REGULAR', '정시근로', '8시간 정규 근무'), ('OVERTIME', '연장근로', '8시간 초과 근무'), ('PARTIAL', '부분근로', '8시간 미만 근무'), ('VACATION', '휴가근로', '휴가와 함께하는 부분 근무') ON DUPLICATE KEY UPDATE type_name = VALUES(type_name), description = VALUES(description); -- 휴가 유형 기본 데이터 INSERT INTO vacation_types (type_code, type_name, hours_deduction, description) VALUES ('ANNUAL_FULL', '연차', 8.0, '하루 전체 연차'), ('ANNUAL_HALF', '반차', 4.0, '반일 연차'), ('ANNUAL_QUARTER', '반반차', 2.0, '1/4일 연차'), ('SICK_FULL', '병가', 8.0, '하루 전체 병가'), ('SICK_HALF', '반일병가', 4.0, '반일 병가'), ('PERSONAL_FULL', '개인사유', 8.0, '개인사유로 인한 휴가'), ('PERSONAL_HALF', '반일개인사유', 4.0, '반일 개인사유 휴가') ON DUPLICATE KEY UPDATE type_name = VALUES(type_name), hours_deduction = VALUES(hours_deduction), description = VALUES(description); -- 6. daily_work_reports 테이블에 근태 기록 연결 컬럼 추가 ALTER TABLE daily_work_reports ADD COLUMN attendance_record_id INT NULL COMMENT '근태 기록 ID' AFTER updated_by, ADD INDEX idx_attendance_record (attendance_record_id); -- 외래키 제약조건 추가 (나중에 데이터 정리 후) -- ALTER TABLE daily_work_reports -- ADD FOREIGN KEY (attendance_record_id) REFERENCES daily_attendance_records(id); -- 7. 휴가 전용 작업 유형 추가 (work_types 테이블에) INSERT INTO work_types (name, description, is_active) VALUES ('휴가', '연차, 반차, 병가 등 휴가 처리용', TRUE) ON DUPLICATE KEY UPDATE description = VALUES(description), is_active = VALUES(is_active); -- 8. 뷰 생성 - 일일 근태 현황 조회용 CREATE OR REPLACE VIEW v_daily_attendance_summary AS SELECT dar.id, dar.record_date, dar.worker_id, w.worker_name, w.job_type, dar.total_work_hours, wat.type_name as attendance_type, vt.type_name as vacation_type, dar.is_vacation_processed, dar.overtime_approved, dar.status, CASE WHEN dar.status = 'incomplete' THEN '미입력' WHEN dar.status = 'partial' THEN '부분입력' WHEN dar.status = 'complete' THEN '정시근로' WHEN dar.status = 'overtime' THEN '연장근로' WHEN dar.status = 'vacation' THEN '휴가' WHEN dar.status = 'error' THEN '오류' ELSE '알수없음' END as status_text, dar.notes, dar.created_at, dar.updated_at FROM daily_attendance_records dar LEFT JOIN workers w ON dar.worker_id = w.worker_id LEFT JOIN work_attendance_types wat ON dar.attendance_type_id = wat.id LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id ORDER BY dar.record_date DESC, w.worker_name; -- 9. 트리거 생성 - daily_work_reports 변경 시 근태 기록 자동 업데이트 DELIMITER // CREATE OR REPLACE TRIGGER tr_update_attendance_on_work_report AFTER INSERT ON daily_work_reports FOR EACH ROW BEGIN DECLARE total_hours DECIMAL(4,2); DECLARE attendance_type INT; DECLARE vacation_type INT; DECLARE record_status VARCHAR(20); DECLARE existing_record_id INT; -- 해당 작업자의 해당 날짜 총 작업시간 계산 SELECT COALESCE(SUM(work_hours), 0) INTO total_hours FROM daily_work_reports WHERE worker_id = NEW.worker_id AND report_date = NEW.report_date; -- 휴가 처리 여부 확인 (work_type_id가 휴가용인지) SELECT id INTO vacation_type FROM vacation_types WHERE (total_hours = 0 AND type_code = 'ANNUAL_FULL') OR (total_hours = 4 AND type_code = 'ANNUAL_HALF') OR (total_hours = 6 AND type_code = 'ANNUAL_QUARTER') LIMIT 1; -- 근로 유형 결정 IF total_hours = 0 THEN SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'PARTIAL'; SET record_status = 'incomplete'; ELSEIF total_hours < 8 THEN SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'PARTIAL'; SET record_status = 'partial'; ELSEIF total_hours = 8 THEN SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'REGULAR'; SET record_status = 'complete'; ELSE SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'OVERTIME'; SET record_status = 'overtime'; END IF; -- 휴가 처리된 경우 상태 조정 IF vacation_type IS NOT NULL THEN SET record_status = 'vacation'; END IF; -- 기존 근태 기록 확인 SELECT id INTO existing_record_id FROM daily_attendance_records WHERE worker_id = NEW.worker_id AND record_date = NEW.report_date; -- 근태 기록 업데이트 또는 생성 IF existing_record_id IS NOT NULL THEN UPDATE daily_attendance_records SET total_work_hours = total_hours, attendance_type_id = attendance_type, vacation_type_id = vacation_type, is_vacation_processed = (vacation_type IS NOT NULL), status = record_status, updated_by = NEW.created_by, updated_at = CURRENT_TIMESTAMP WHERE id = existing_record_id; ELSE INSERT INTO daily_attendance_records ( record_date, worker_id, total_work_hours, attendance_type_id, vacation_type_id, is_vacation_processed, status, created_by ) VALUES ( NEW.report_date, NEW.worker_id, total_hours, attendance_type, vacation_type, (vacation_type IS NOT NULL), record_status, NEW.created_by ); END IF; END// DELIMITER ; -- 10. 기존 데이터 마이그레이션을 위한 프로시저 DELIMITER // CREATE OR REPLACE PROCEDURE sp_migrate_existing_work_reports() BEGIN DECLARE done INT DEFAULT FALSE; DECLARE v_worker_id INT; DECLARE v_report_date DATE; DECLARE cur CURSOR FOR SELECT DISTINCT worker_id, report_date FROM daily_work_reports ORDER BY report_date DESC, worker_id; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN cur; read_loop: LOOP FETCH cur INTO v_worker_id, v_report_date; IF done THEN LEAVE read_loop; END IF; -- 각 작업자별 날짜별로 근태 기록 생성/업데이트 CALL sp_update_attendance_record(v_worker_id, v_report_date); END LOOP; CLOSE cur; END// CREATE OR REPLACE PROCEDURE sp_update_attendance_record( IN p_worker_id INT, IN p_report_date DATE ) BEGIN DECLARE total_hours DECIMAL(4,2); DECLARE attendance_type INT; DECLARE vacation_type INT; DECLARE record_status VARCHAR(20); DECLARE existing_record_id INT; DECLARE has_vacation_work INT DEFAULT 0; -- 해당 작업자의 해당 날짜 총 작업시간 계산 SELECT COALESCE(SUM(work_hours), 0) INTO total_hours FROM daily_work_reports WHERE worker_id = p_worker_id AND report_date = p_report_date; -- 휴가 관련 작업이 있는지 확인 (work_type_id = 999 또는 휴가 관련) SELECT COUNT(*) INTO has_vacation_work FROM daily_work_reports dwr JOIN work_types wt ON dwr.work_type_id = wt.id WHERE dwr.worker_id = p_worker_id AND dwr.report_date = p_report_date AND (wt.name LIKE '%휴가%' OR wt.name LIKE '%연차%' OR wt.name LIKE '%반차%'); -- 휴가 유형 결정 IF has_vacation_work > 0 THEN IF total_hours = 0 THEN SELECT id INTO vacation_type FROM vacation_types WHERE type_code = 'ANNUAL_FULL' LIMIT 1; ELSEIF total_hours = 4 THEN SELECT id INTO vacation_type FROM vacation_types WHERE type_code = 'ANNUAL_HALF' LIMIT 1; ELSEIF total_hours = 6 THEN SELECT id INTO vacation_type FROM vacation_types WHERE type_code = 'ANNUAL_QUARTER' LIMIT 1; END IF; END IF; -- 근로 유형 및 상태 결정 IF vacation_type IS NOT NULL THEN SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'VACATION'; SET record_status = 'vacation'; ELSEIF total_hours = 0 THEN SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'PARTIAL'; SET record_status = 'incomplete'; ELSEIF total_hours < 8 THEN SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'PARTIAL'; SET record_status = 'partial'; ELSEIF total_hours = 8 THEN SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'REGULAR'; SET record_status = 'complete'; ELSE SELECT id INTO attendance_type FROM work_attendance_types WHERE type_code = 'OVERTIME'; SET record_status = 'overtime'; END IF; -- 기존 근태 기록 확인 SELECT id INTO existing_record_id FROM daily_attendance_records WHERE worker_id = p_worker_id AND record_date = p_report_date; -- 근태 기록 업데이트 또는 생성 IF existing_record_id IS NOT NULL THEN UPDATE daily_attendance_records SET total_work_hours = total_hours, attendance_type_id = attendance_type, vacation_type_id = vacation_type, is_vacation_processed = (vacation_type IS NOT NULL), status = record_status, updated_by = 1, updated_at = CURRENT_TIMESTAMP WHERE id = existing_record_id; ELSE INSERT INTO daily_attendance_records ( record_date, worker_id, total_work_hours, attendance_type_id, vacation_type_id, is_vacation_processed, status, created_by ) VALUES ( p_report_date, p_worker_id, total_hours, attendance_type, vacation_type, (vacation_type IS NOT NULL), record_status, 1 ); END IF; END// DELIMITER ; -- 11. 권한 및 인덱스 최적화 -- 추가 인덱스 생성 CREATE INDEX idx_daily_work_reports_worker_date ON daily_work_reports(worker_id, report_date); CREATE INDEX idx_daily_work_reports_work_type ON daily_work_reports(work_type_id); -- 마이그레이션 실행 (주석 해제하여 실행) -- CALL sp_migrate_existing_work_reports(); -- 완료 메시지 SELECT 'DB 확장 완료: 근로 및 휴가 관리 시스템이 성공적으로 구축되었습니다.' as message;