refactor: 네비게이션 헤더 최신 디자인으로 전면 개편 및 로그인 버그 수정

- fix: 로그인 API에서 user.role_name 필드 올바르게 사용 (auth.service.js)
- refactor: navbar 컴포넌트를 최신 dashboard-header 스타일로 전환
- refactor: 구버전 work-report-header 제거 (6개 페이지)
- refactor: load-navbar.js를 최신 헤더 구조에 맞게 업데이트
- style: 파란색 그라데이션 헤더, 실시간 시계, 향상된 프로필 메뉴
- docs: 2026-01-20 개발 로그 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-01-20 08:40:19 +09:00
parent 6933f67a2e
commit 4ac0605887
24 changed files with 1614 additions and 524 deletions

View File

@@ -35,19 +35,20 @@ const getAllUsers = asyncHandler(async (req, res) => {
try {
const query = `
SELECT
user_id,
username,
name,
email,
phone,
role,
access_level,
is_active,
created_at,
updated_at,
last_login
FROM users
ORDER BY created_at DESC
u.user_id,
u.username,
u.name,
u.email,
u.role_id,
r.name as role,
u._access_level_old as access_level,
u.is_active,
u.created_at,
u.updated_at,
u.last_login_at as last_login
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
ORDER BY u.created_at DESC
`;
const [users] = await db.execute(query);

View File

@@ -23,13 +23,13 @@ exports.up = async function(knex) {
// 2. 사용자별 페이지 접근 권한 테이블 생성
await knex.schema.createTable('user_page_access', function(table) {
table.integer('user_id').unsigned().notNullable()
table.integer('user_id').notNullable()
.references('user_id').inTable('users').onDelete('CASCADE');
table.integer('page_id').unsigned().notNullable()
.references('id').inTable('pages').onDelete('CASCADE');
table.boolean('can_access').defaultTo(true); // 접근 가능 여부
table.timestamp('granted_at').defaultTo(knex.fn.now());
table.integer('granted_by').unsigned() // 권한을 부여한 Admin의 user_id
table.integer('granted_by') // 권한을 부여한 Admin의 user_id
.references('user_id').inTable('users').onDelete('SET NULL');
table.primary(['user_id', 'page_id']);
});

View File

@@ -7,7 +7,7 @@
* 2. 각 작업자에 대해 users 테이블에 계정 생성
* 3. username은 이름 기반으로 자동 생성 (예: 홍길동 → hong.gildong)
* 4. 초기 비밀번호는 '1234'로 통일 (첫 로그인 시 변경 권장)
* 5. 현재 연도 연차 잔액 초기화
* 5. 현재 연도 연차 잔액 초기화 (workers.annual_leave 사용)
*/
const bcrypt = require('bcrypt');
@@ -25,7 +25,7 @@ exports.up = async function(knex) {
'workers.worker_name',
'workers.email',
'workers.status',
'workers.base_annual_leave'
'workers.annual_leave'
);
console.log(`📊 계정이 없는 작업자: ${workersWithoutAccount.length}`);
@@ -41,7 +41,7 @@ exports.up = async function(knex) {
// User 역할 ID 조회
const userRole = await knex('roles')
.where('role_name', 'User')
.where('name', 'User')
.first();
if (!userRole) {
@@ -77,7 +77,7 @@ exports.up = async function(knex) {
await knex('worker_vacation_balance').insert({
worker_id: worker.worker_id,
year: currentYear,
total_annual_leave: worker.base_annual_leave || 15,
total_annual_leave: worker.annual_leave || 15,
used_annual_leave: 0,
created_at: knex.fn.now(),
updated_at: knex.fn.now()

View File

@@ -12,8 +12,8 @@ exports.up = async function(knex) {
// 1. Guest 역할 추가
const [guestRoleId] = await knex('roles').insert({
role_name: 'Guest',
role_description: '게스트 (계정 없이 특정 기능 접근 가능)',
name: 'Guest',
description: '게스트 (계정 없이 특정 기능 접근 가능)',
created_at: knex.fn.now(),
updated_at: knex.fn.now()
});
@@ -44,7 +44,7 @@ exports.down = async function(knex) {
// 역할 제거
await knex('roles')
.where('role_name', 'Guest')
.where('name', 'Guest')
.delete();
console.log('✅ 게스트 역할 제거 완료');

0
api.hyungi.net/deploy.sh Normal file → Executable file
View File

View File

@@ -129,8 +129,10 @@ const requireRole = (...roles) => {
}
const userRole = req.user.role;
const userRoleLower = userRole ? userRole.toLowerCase() : '';
const rolesLower = roles.map(r => r.toLowerCase());
if (!roles.includes(userRole)) {
if (!rolesLower.includes(userRoleLower)) {
logger.warn('권한 체크 실패: 역할 불일치', {
user_id: req.user.user_id || req.user.id,
username: req.user.username,

View File

@@ -5,7 +5,7 @@ class AttendanceModel {
static async getDailyAttendanceRecords(date, workerId = null) {
const db = await getDb();
let query = `
SELECT
SELECT
dar.*,
w.worker_name,
w.job_type,
@@ -34,6 +34,39 @@ class AttendanceModel {
return rows;
}
// 기간별 근태 기록 조회 (월별 조회용)
static async getDailyRecords(startDate, endDate, workerId = null) {
const db = await getDb();
let query = `
SELECT
dar.*,
w.worker_name,
w.job_type,
wat.type_name as attendance_type_name,
wat.type_code as attendance_type_code,
vt.type_name as vacation_type_name,
vt.type_code as vacation_type_code,
vt.deduct_days as vacation_days
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
WHERE dar.record_date BETWEEN ? AND ?
`;
const params = [startDate, endDate];
if (workerId) {
query += ' AND dar.worker_id = ?';
params.push(workerId);
}
query += ' ORDER BY dar.record_date ASC';
const [rows] = await db.execute(query, params);
return rows;
}
// 작업 보고서와 근태 기록 동기화 (시간 합산 및 상태 업데이트)
static async syncWithWorkReports(workerId, date) {
const db = await getDb();

View File

@@ -5,7 +5,14 @@ const findByUsername = async (username) => {
try {
const db = await getDb();
const [rows] = await db.query(
'SELECT user_id, username, password, name, email, role, access_level, worker_id, is_active, last_login_at, password_changed_at, failed_login_attempts, locked_until, created_at, updated_at FROM users WHERE username = ?', [username]
`SELECT u.user_id, u.username, u.password, u.name, u.email,
u.role_id, r.name as role_name,
u._access_level_old as access_level, u.worker_id, u.is_active,
u.last_login_at, u.password_changed_at, u.failed_login_attempts,
u.locked_until, u.created_at, u.updated_at
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
WHERE u.username = ?`, [username]
);
return rows[0];
} catch (err) {

View File

@@ -22,7 +22,8 @@ router.use(verifyToken);
* 관리자 권한 확인 미들웨어
*/
const adminOnly = (req, res, next) => {
if (req.user && (req.user.role === 'admin' || req.user.role === 'system')) {
const userRole = req.user?.role?.toLowerCase();
if (req.user && (userRole === 'admin' || userRole === 'system' || userRole === 'system admin')) {
next();
} else {
logger.warn('관리자 권한 없는 접근 시도', {
@@ -87,8 +88,9 @@ router.get('/me/work-reports', async (req, res) => {
router.get('/me/monthly-stats', async (req, res) => {
try {
const { year, month } = req.query;
const db = require('../config/database');
const stats = await db.query(
const { getDb } = require('../dbPool');
const db = await getDb();
const [stats] = await db.execute(
`SELECT
SUM(total_work_hours) as month_hours,
COUNT(DISTINCT record_date) as work_days

View File

@@ -57,7 +57,7 @@ const loginService = async (username, password, ipAddress, userAgent) => {
const token = jwt.sign(
{ user_id: user.user_id, username: user.username, role: user.role, access_level: user.access_level, worker_id: user.worker_id, name: user.name || user.username },
{ user_id: user.user_id, username: user.username, role: user.role_name, role_id: user.role_id, access_level: user.access_level, worker_id: user.worker_id, name: user.name || user.username },
process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
);
@@ -80,7 +80,7 @@ const loginService = async (username, password, ipAddress, userAgent) => {
user_id: user.user_id,
username: user.username,
name: user.name || user.username,
role: user.role,
role: user.role_name,
access_level: user.access_level,
worker_id: user.worker_id
}