feat: 작업자-계정 통합 및 연차/출근 관리 시스템 구축
모든 작업자가 개인 계정으로 로그인하여 본인의 연차와 출근 기록을 확인할 수 있는 시스템을 구축했습니다. 주요 기능: - 작업자-계정 1:1 통합 (기존 작업자 자동 계정 생성) - 연차 관리 시스템 (연도별 잔액 관리) - 출근 기록 시스템 (일일 근태 기록) - 나의 대시보드 페이지 (개인 정보 조회) 데이터베이스: - workers 테이블에 salary, base_annual_leave 컬럼 추가 - work_attendance_types, vacation_types 테이블 생성 - daily_attendance_records 테이블 생성 - worker_vacation_balance 테이블 생성 - 기존 작업자 자동 계정 생성 (username: 이름 기반) - Guest 역할 추가 백엔드 API: - 한글→영문 변환 유틸리티 (hangulToRoman.js) - UserRoutes에 개인 정보 조회 API 추가 - GET /api/users/me (내 정보) - GET /api/users/me/attendance-records (출근 기록) - GET /api/users/me/vacation-balance (연차 잔액) - GET /api/users/me/work-reports (작업 보고서) - GET /api/users/me/monthly-stats (월별 통계) 프론트엔드: - 나의 대시보드 페이지 (my-dashboard.html) - 연차 정보 위젯 (총/사용/잔여) - 월별 출근 캘린더 - 근무 시간 통계 - 최근 작업 보고서 목록 - 네비게이션 바에 "나의 대시보드" 메뉴 추가 배포 시 주의사항: - 마이그레이션 실행 필요 - 자동 생성된 계정 초기 비밀번호: 1234 - 작업자들에게 첫 로그인 후 비밀번호 변경 안내 필요 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
156
api.hyungi.net/utils/hangulToRoman.js
Normal file
156
api.hyungi.net/utils/hangulToRoman.js
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 한글 이름을 영문(로마자)으로 변환하는 유틸리티
|
||||
*
|
||||
* 사용 예시:
|
||||
* hangulToRoman('홍길동') => 'hong.gildong'
|
||||
* hangulToRoman('김철수') => 'kim.cheolsu'
|
||||
*/
|
||||
|
||||
// 한글 로마자 변환 매핑 (국립국어원 표기법 기준)
|
||||
const CHOSUNG_MAP = {
|
||||
'ㄱ': 'g', 'ㄲ': 'kk', 'ㄴ': 'n', 'ㄷ': 'd', 'ㄸ': 'tt',
|
||||
'ㄹ': 'r', 'ㅁ': 'm', 'ㅂ': 'b', 'ㅃ': 'pp', 'ㅅ': 's',
|
||||
'ㅆ': 'ss', 'ㅇ': '', 'ㅈ': 'j', 'ㅉ': 'jj', 'ㅊ': 'ch',
|
||||
'ㅋ': 'k', 'ㅌ': 't', 'ㅍ': 'p', 'ㅎ': 'h'
|
||||
};
|
||||
|
||||
const JUNGSUNG_MAP = {
|
||||
'ㅏ': 'a', 'ㅐ': 'ae', 'ㅑ': 'ya', 'ㅒ': 'yae', 'ㅓ': 'eo',
|
||||
'ㅔ': 'e', 'ㅕ': 'yeo', 'ㅖ': 'ye', 'ㅗ': 'o', 'ㅘ': 'wa',
|
||||
'ㅙ': 'wae', 'ㅚ': 'oe', 'ㅛ': 'yo', 'ㅜ': 'u', 'ㅝ': 'wo',
|
||||
'ㅞ': 'we', 'ㅟ': 'wi', 'ㅠ': 'yu', 'ㅡ': 'eu', 'ㅢ': 'ui',
|
||||
'ㅣ': 'i'
|
||||
};
|
||||
|
||||
const JONGSUNG_MAP = {
|
||||
'': '', 'ㄱ': 'k', 'ㄲ': 'k', 'ㄳ': 'k', 'ㄴ': 'n', 'ㄵ': 'n',
|
||||
'ㄶ': 'n', 'ㄷ': 't', 'ㄹ': 'l', 'ㄺ': 'k', 'ㄻ': 'm', 'ㄼ': 'p',
|
||||
'ㄽ': 'l', 'ㄾ': 'l', 'ㄿ': 'p', 'ㅀ': 'l', 'ㅁ': 'm', 'ㅂ': 'p',
|
||||
'ㅄ': 'p', 'ㅅ': 't', 'ㅆ': 't', 'ㅇ': 'ng', 'ㅈ': 't', 'ㅊ': 't',
|
||||
'ㅋ': 'k', 'ㅌ': 't', 'ㅍ': 'p', 'ㅎ': 't'
|
||||
};
|
||||
|
||||
const CHOSUNG = [
|
||||
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ',
|
||||
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
|
||||
];
|
||||
|
||||
const JUNGSUNG = [
|
||||
'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ',
|
||||
'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ'
|
||||
];
|
||||
|
||||
const JONGSUNG = [
|
||||
'', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ',
|
||||
'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ',
|
||||
'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
|
||||
];
|
||||
|
||||
/**
|
||||
* 한글 한 글자를 초성, 중성, 종성으로 분리
|
||||
* @param {string} char - 한글 한 글자
|
||||
* @returns {object} { cho, jung, jong }
|
||||
*/
|
||||
function decomposeHangul(char) {
|
||||
const code = char.charCodeAt(0) - 0xAC00;
|
||||
|
||||
if (code < 0 || code > 11171) {
|
||||
return null; // 한글이 아님
|
||||
}
|
||||
|
||||
const choIndex = Math.floor(code / 588);
|
||||
const jungIndex = Math.floor((code % 588) / 28);
|
||||
const jongIndex = code % 28;
|
||||
|
||||
return {
|
||||
cho: CHOSUNG[choIndex],
|
||||
jung: JUNGSUNG[jungIndex],
|
||||
jong: JONGSUNG[jongIndex]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 한글 이름을 로마자로 변환
|
||||
* @param {string} koreanName - 한글 이름
|
||||
* @returns {string} 로마자 이름 (예: 'hong.gildong')
|
||||
*/
|
||||
function hangulToRoman(koreanName) {
|
||||
if (!koreanName || typeof koreanName !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 공백 제거
|
||||
const trimmed = koreanName.trim();
|
||||
|
||||
// 성과 이름 분리 (첫 글자를 성으로 간주)
|
||||
const surname = trimmed[0];
|
||||
const givenName = trimmed.substring(1);
|
||||
|
||||
// 각 부분을 로마자로 변환
|
||||
const romanSurname = convertToRoman(surname);
|
||||
const romanGivenName = convertToRoman(givenName);
|
||||
|
||||
// 점(.)으로 연결
|
||||
return `${romanSurname}.${romanGivenName}`.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 한글 문자열을 로마자로 변환
|
||||
* @param {string} text - 한글 문자열
|
||||
* @returns {string} 로마자 문자열
|
||||
*/
|
||||
function convertToRoman(text) {
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
const decomposed = decomposeHangul(char);
|
||||
|
||||
if (decomposed) {
|
||||
result += CHOSUNG_MAP[decomposed.cho] || '';
|
||||
result += JUNGSUNG_MAP[decomposed.jung] || '';
|
||||
result += JONGSUNG_MAP[decomposed.jong] || '';
|
||||
} else {
|
||||
// 한글이 아닌 경우 그대로 추가
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자명 생성 (중복 확인 및 처리)
|
||||
* @param {string} koreanName - 한글 이름
|
||||
* @param {object} knex - Knex 인스턴스
|
||||
* @returns {Promise<string>} 고유한 username
|
||||
*/
|
||||
async function generateUniqueUsername(koreanName, knex) {
|
||||
const baseUsername = hangulToRoman(koreanName);
|
||||
let username = baseUsername;
|
||||
let counter = 1;
|
||||
|
||||
// 중복 확인
|
||||
while (true) {
|
||||
const existing = await knex('users')
|
||||
.where('username', username)
|
||||
.first();
|
||||
|
||||
if (!existing) {
|
||||
break; // 중복 없음
|
||||
}
|
||||
|
||||
// 중복 시 숫자 추가
|
||||
username = `${baseUsername}${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hangulToRoman,
|
||||
convertToRoman,
|
||||
generateUniqueUsername,
|
||||
decomposeHangul
|
||||
};
|
||||
Reference in New Issue
Block a user