diff --git a/system2-report/web/pages/safety/my-reports.html b/system2-report/web/pages/safety/my-reports.html
new file mode 100644
index 0000000..4aa698a
--- /dev/null
+++ b/system2-report/web/pages/safety/my-reports.html
@@ -0,0 +1,327 @@
+
+
+
+
+
+
+
+
diff --git a/system3-nonconformance/api/database/models.py b/system3-nonconformance/api/database/models.py
index 2c21835..6ab7da3 100644
--- a/system3-nonconformance/api/database/models.py
+++ b/system3-nonconformance/api/database/models.py
@@ -68,6 +68,7 @@ class User(Base):
# Relationships
issues = relationship("Issue", back_populates="reporter", foreign_keys="Issue.reporter_id")
reviewed_issues = relationship("Issue", foreign_keys="Issue.reviewed_by_id")
+ daily_works = relationship("DailyWork", back_populates="created_by")
page_permissions = relationship("UserPagePermission", back_populates="user", foreign_keys="UserPagePermission.user_id")
class UserPagePermission(Base):
@@ -104,6 +105,7 @@ class Issue(Base):
status = Column(Enum(IssueStatus), default=IssueStatus.new)
reporter_id = Column(Integer, ForeignKey("sso_users.user_id"))
project_id = Column(Integer) # FK 제거 — projects는 tkuser에서 관리
+ location_info = Column(String(200)) # 공장/작업장 위치 정보 (System 2 연동)
report_date = Column(DateTime, default=get_kst_now)
work_hours = Column(Float, default=0)
detail_notes = Column(Text)
@@ -182,6 +184,37 @@ class Project(Base):
primaryjoin="Project.id == Issue.project_id",
foreign_keys="[Issue.project_id]")
+class DailyWork(Base):
+ __tablename__ = "qc_daily_works"
+
+ id = Column(Integer, primary_key=True, index=True)
+ date = Column(DateTime, nullable=False, index=True)
+ worker_count = Column(Integer, nullable=False)
+ regular_hours = Column(Float, nullable=False)
+ overtime_workers = Column(Integer, default=0)
+ overtime_hours = Column(Float, default=0)
+ overtime_total = Column(Float, default=0)
+ total_hours = Column(Float, nullable=False)
+ created_by_id = Column(Integer, ForeignKey("sso_users.user_id"))
+ created_at = Column(DateTime, default=get_kst_now)
+
+ # Relationships
+ created_by = relationship("User", back_populates="daily_works")
+
+class ProjectDailyWork(Base):
+ __tablename__ = "qc_project_daily_works"
+
+ id = Column(Integer, primary_key=True, index=True)
+ date = Column(DateTime, nullable=False, index=True)
+ project_id = Column(Integer, ForeignKey("projects.project_id"), nullable=False)
+ hours = Column(Float, nullable=False)
+ created_by_id = Column(Integer, ForeignKey("sso_users.user_id"))
+ created_at = Column(DateTime, default=get_kst_now)
+
+ # Relationships
+ project = relationship("Project")
+ created_by = relationship("User")
+
class DeletionLog(Base):
__tablename__ = "qc_deletion_logs"
diff --git a/system3-nonconformance/api/database/schemas.py b/system3-nonconformance/api/database/schemas.py
index aabac7c..bce2760 100644
--- a/system3-nonconformance/api/database/schemas.py
+++ b/system3-nonconformance/api/database/schemas.py
@@ -91,6 +91,7 @@ class IssueBase(BaseModel):
project_id: int
class IssueCreate(IssueBase):
+ location_info: Optional[str] = None # 공장/작업장 위치 정보
photo: Optional[str] = None # Base64 encoded image
photo2: Optional[str] = None
photo3: Optional[str] = None
@@ -112,6 +113,7 @@ class IssueUpdate(BaseModel):
class Issue(IssueBase):
id: int
+ location_info: Optional[str] = None
photo_path: Optional[str] = None
photo_path2: Optional[str] = None
photo_path3: Optional[str] = None
@@ -259,6 +261,7 @@ class InboxIssue(BaseModel):
id: int
category: IssueCategory
description: str
+ location_info: Optional[str] = None
photo_path: Optional[str] = None
photo_path2: Optional[str] = None
project_id: Optional[int] = None
@@ -300,6 +303,50 @@ class Project(ProjectBase):
class Config:
from_attributes = True
+# DailyWork schemas
+class DailyWorkBase(BaseModel):
+ date: datetime
+ worker_count: int = Field(gt=0)
+ overtime_workers: Optional[int] = 0
+ overtime_hours: Optional[float] = 0
+
+class DailyWorkCreate(DailyWorkBase):
+ pass
+
+class DailyWorkUpdate(BaseModel):
+ worker_count: Optional[int] = Field(None, gt=0)
+ overtime_workers: Optional[int] = None
+ overtime_hours: Optional[float] = None
+
+class DailyWork(DailyWorkBase):
+ id: int
+ regular_hours: float
+ overtime_total: float
+ total_hours: float
+ created_by_id: int
+ created_by: User
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+
+class ProjectDailyWorkBase(BaseModel):
+ date: datetime
+ project_id: int
+ hours: float
+
+class ProjectDailyWorkCreate(ProjectDailyWorkBase):
+ pass
+
+class ProjectDailyWork(ProjectDailyWorkBase):
+ id: int
+ created_by_id: int
+ created_at: datetime
+ project: Project
+
+ class Config:
+ from_attributes = True
+
# Report schemas
class ReportRequest(BaseModel):
start_date: datetime
diff --git a/system3-nonconformance/api/routers/issues.py b/system3-nonconformance/api/routers/issues.py
index 02b6b0a..08dcfe4 100644
--- a/system3-nonconformance/api/routers/issues.py
+++ b/system3-nonconformance/api/routers/issues.py
@@ -35,6 +35,7 @@ async def create_issue(
db_issue = Issue(
category=issue.category,
description=issue.description,
+ location_info=issue.location_info,
photo_path=photo_paths.get('photo_path'),
photo_path2=photo_paths.get('photo_path2'),
photo_path3=photo_paths.get('photo_path3'),
diff --git a/system3-nonconformance/web/static/js/pages/issue-view.js b/system3-nonconformance/web/static/js/pages/issue-view.js
index d83daff..6bdf11f 100644
--- a/system3-nonconformance/web/static/js/pages/issue-view.js
+++ b/system3-nonconformance/web/static/js/pages/issue-view.js
@@ -564,6 +564,8 @@ function createIssueCard(issue, isCompleted) {
${(() => {
diff --git a/user-management/api/controllers/permissionController.js b/user-management/api/controllers/permissionController.js
index e520980..2849488 100644
--- a/user-management/api/controllers/permissionController.js
+++ b/user-management/api/controllers/permissionController.js
@@ -149,11 +149,77 @@ async function deletePermission(req, res, next) {
}
}
+/**
+ * GET /api/permissions/departments/:deptId/permissions - 부서 권한 조회
+ */
+async function getDepartmentPermissions(req, res, next) {
+ try {
+ const deptId = parseInt(req.params.deptId);
+ const permissions = await permissionModel.getDepartmentPermissions(deptId);
+ res.json({ success: true, data: permissions });
+ } catch (err) {
+ next(err);
+ }
+}
+
+/**
+ * POST /api/permissions/departments/:deptId/bulk-set - 부서 권한 일괄 설정
+ */
+async function bulkSetDepartmentPermissions(req, res, next) {
+ try {
+ const deptId = parseInt(req.params.deptId);
+ const { permissions } = req.body;
+ const grantedById = req.user.user_id || req.user.id;
+
+ const result = await permissionModel.bulkSetDepartmentPermissions({
+ department_id: deptId,
+ permissions,
+ granted_by_id: grantedById
+ });
+
+ res.json({
+ success: true,
+ message: `${result.updated_count}개의 부서 권한이 설정되었습니다`,
+ updated_count: result.updated_count
+ });
+ } catch (err) {
+ next(err);
+ }
+}
+
+/**
+ * GET /api/permissions/users/:userId/effective-permissions - 출처 포함 권한 조회
+ */
+async function getUserPermissionsWithSource(req, res, next) {
+ try {
+ const userId = parseInt(req.params.userId);
+ const requesterId = req.user.user_id || req.user.id;
+
+ // 관리자이거나 본인만 조회 가능
+ if (req.user.role !== 'admin' && requesterId !== userId) {
+ return res.status(403).json({ success: false, error: '권한이 없습니다' });
+ }
+
+ const user = await userModel.findById(userId);
+ if (!user) {
+ return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
+ }
+
+ const result = await permissionModel.getUserPermissionsWithSource(userId);
+ res.json({ success: true, ...result });
+ } catch (err) {
+ next(err);
+ }
+}
+
module.exports = {
getUserPermissions,
grantPermission,
bulkGrant,
checkAccess,
getAvailablePages,
- deletePermission
+ deletePermission,
+ getDepartmentPermissions,
+ bulkSetDepartmentPermissions,
+ getUserPermissionsWithSource
};
diff --git a/user-management/api/controllers/userController.js b/user-management/api/controllers/userController.js
index 82f504e..d4eb0fb 100644
--- a/user-management/api/controllers/userController.js
+++ b/user-management/api/controllers/userController.js
@@ -23,7 +23,7 @@ async function getUsers(req, res, next) {
*/
async function createUser(req, res, next) {
try {
- const { username, password, name, full_name, department, role } = req.body;
+ const { username, password, name, full_name, department, department_id, role } = req.body;
if (!username || !password) {
return res.status(400).json({ success: false, error: '사용자명과 비밀번호는 필수입니다' });
@@ -39,6 +39,7 @@ async function createUser(req, res, next) {
password,
name: name || full_name,
department,
+ department_id: department_id || null,
role
});
res.status(201).json({ success: true, data: user });
diff --git a/user-management/api/models/permissionModel.js b/user-management/api/models/permissionModel.js
index c879921..baca921 100644
--- a/user-management/api/models/permissionModel.js
+++ b/user-management/api/models/permissionModel.js
@@ -104,20 +104,36 @@ async function bulkGrant({ user_id, permissions, granted_by_id }) {
}
/**
- * 접근 권한 확인
+ * 접근 권한 확인 (우선순위: 개인 > 부서 > 기본값)
*/
async function checkAccess(userId, pageName) {
const db = getPool();
+
+ // 1. 명시적 개인 권한
const [rows] = await db.query(
'SELECT can_access FROM user_page_permissions WHERE user_id = ? AND page_name = ?',
[userId, pageName]
);
-
if (rows.length > 0) {
return { can_access: rows[0].can_access, reason: 'explicit_permission' };
}
- // 기본 권한
+ // 2. 부서 권한
+ const [userRows] = await db.query(
+ 'SELECT department_id FROM sso_users WHERE user_id = ?',
+ [userId]
+ );
+ if (userRows.length > 0 && userRows[0].department_id) {
+ const [deptRows] = await db.query(
+ 'SELECT can_access FROM department_page_permissions WHERE department_id = ? AND page_name = ?',
+ [userRows[0].department_id, pageName]
+ );
+ if (deptRows.length > 0) {
+ return { can_access: deptRows[0].can_access, reason: 'department_permission' };
+ }
+ }
+
+ // 3. 기본 권한
const pageConfig = DEFAULT_PAGES[pageName];
if (!pageConfig) {
return { can_access: false, reason: 'invalid_page' };
@@ -125,6 +141,110 @@ async function checkAccess(userId, pageName) {
return { can_access: pageConfig.default_access, reason: 'default_permission' };
}
+/**
+ * 부서별 페이지 권한 조회
+ */
+async function getDepartmentPermissions(departmentId) {
+ const db = getPool();
+ const [rows] = await db.query(
+ 'SELECT * FROM department_page_permissions WHERE department_id = ? ORDER BY page_name',
+ [departmentId]
+ );
+ return rows;
+}
+
+/**
+ * 부서 권한 단건 UPSERT
+ */
+async function setDepartmentPermission({ department_id, page_name, can_access, granted_by_id }) {
+ const db = getPool();
+ const [result] = await db.query(
+ `INSERT INTO department_page_permissions (department_id, page_name, can_access, granted_by_id)
+ VALUES (?, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE can_access = VALUES(can_access), granted_by_id = VALUES(granted_by_id), granted_at = CURRENT_TIMESTAMP`,
+ [department_id, page_name, can_access, granted_by_id]
+ );
+ return { id: result.insertId, department_id, page_name, can_access };
+}
+
+/**
+ * 부서 권한 일괄 설정
+ */
+async function bulkSetDepartmentPermissions({ department_id, permissions, granted_by_id }) {
+ const db = getPool();
+ let count = 0;
+
+ for (const perm of permissions) {
+ if (!DEFAULT_PAGES[perm.page_name]) continue;
+ await db.query(
+ `INSERT INTO department_page_permissions (department_id, page_name, can_access, granted_by_id)
+ VALUES (?, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE can_access = VALUES(can_access), granted_by_id = VALUES(granted_by_id), granted_at = CURRENT_TIMESTAMP`,
+ [department_id, perm.page_name, perm.can_access, granted_by_id]
+ );
+ count++;
+ }
+
+ return { updated_count: count };
+}
+
+/**
+ * 부서 권한 삭제 (기본값으로 복귀)
+ */
+async function deleteDepartmentPermission(departmentId, pageName) {
+ const db = getPool();
+ const [result] = await db.query(
+ 'DELETE FROM department_page_permissions WHERE department_id = ? AND page_name = ?',
+ [departmentId, pageName]
+ );
+ return result.affectedRows > 0;
+}
+
+/**
+ * 사용자 권한 + 출처 조회 (각 페이지별 현재 적용 권한과 출처 반환)
+ */
+async function getUserPermissionsWithSource(userId) {
+ const db = getPool();
+
+ // 개인 권한 조회
+ const [userPerms] = await db.query(
+ 'SELECT page_name, can_access FROM user_page_permissions WHERE user_id = ?',
+ [userId]
+ );
+ const userPermMap = {};
+ userPerms.forEach(p => { userPermMap[p.page_name] = !!p.can_access; });
+
+ // 부서 권한 조회
+ const [userRows] = await db.query(
+ 'SELECT department_id FROM sso_users WHERE user_id = ?',
+ [userId]
+ );
+ const deptId = userRows.length > 0 ? userRows[0].department_id : null;
+
+ const deptPermMap = {};
+ if (deptId) {
+ const [deptPerms] = await db.query(
+ 'SELECT page_name, can_access FROM department_page_permissions WHERE department_id = ?',
+ [deptId]
+ );
+ deptPerms.forEach(p => { deptPermMap[p.page_name] = !!p.can_access; });
+ }
+
+ // 모든 페이지에 대해 결과 조합
+ const result = {};
+ for (const [pageName, config] of Object.entries(DEFAULT_PAGES)) {
+ if (pageName in userPermMap) {
+ result[pageName] = { can_access: userPermMap[pageName], source: 'explicit' };
+ } else if (pageName in deptPermMap) {
+ result[pageName] = { can_access: deptPermMap[pageName], source: 'department' };
+ } else {
+ result[pageName] = { can_access: config.default_access, source: 'default' };
+ }
+ }
+
+ return { permissions: result, department_id: deptId };
+}
+
/**
* 권한 삭제 (기본값으로 되돌림)
*/
@@ -146,5 +266,10 @@ module.exports = {
grantPermission,
bulkGrant,
checkAccess,
- deletePermission
+ deletePermission,
+ getDepartmentPermissions,
+ setDepartmentPermission,
+ bulkSetDepartmentPermissions,
+ deleteDepartmentPermission,
+ getUserPermissionsWithSource
};
diff --git a/user-management/api/models/userModel.js b/user-management/api/models/userModel.js
index 5c5863e..8a6477e 100644
--- a/user-management/api/models/userModel.js
+++ b/user-management/api/models/userModel.js
@@ -97,18 +97,18 @@ async function findById(userId) {
async function findAll() {
const db = getPool();
const [rows] = await db.query(
- 'SELECT user_id, username, name, department, role, system1_access, system2_access, system3_access, is_active, last_login, created_at FROM sso_users ORDER BY user_id'
+ 'SELECT user_id, username, name, department, department_id, role, system1_access, system2_access, system3_access, is_active, last_login, created_at FROM sso_users ORDER BY user_id'
);
return rows;
}
-async function create({ username, password, name, department, role }) {
+async function create({ username, password, name, department, department_id, role }) {
const db = getPool();
const password_hash = await hashPassword(password);
const [result] = await db.query(
- `INSERT INTO sso_users (username, password_hash, name, department, role)
- VALUES (?, ?, ?, ?, ?)`,
- [username, password_hash, name || null, department || null, role || 'user']
+ `INSERT INTO sso_users (username, password_hash, name, department, department_id, role)
+ VALUES (?, ?, ?, ?, ?, ?)`,
+ [username, password_hash, name || null, department || null, department_id || null, role || 'user']
);
return findById(result.insertId);
}
@@ -120,6 +120,7 @@ async function update(userId, data) {
if (data.name !== undefined) { fields.push('name = ?'); values.push(data.name); }
if (data.department !== undefined) { fields.push('department = ?'); values.push(data.department); }
+ if (data.department_id !== undefined) { fields.push('department_id = ?'); values.push(data.department_id); }
if (data.role !== undefined) { fields.push('role = ?'); values.push(data.role); }
if (data.system1_access !== undefined) { fields.push('system1_access = ?'); values.push(data.system1_access); }
if (data.system2_access !== undefined) { fields.push('system2_access = ?'); values.push(data.system2_access); }
diff --git a/user-management/api/routes/permissionRoutes.js b/user-management/api/routes/permissionRoutes.js
index c24d355..f9cd3d4 100644
--- a/user-management/api/routes/permissionRoutes.js
+++ b/user-management/api/routes/permissionRoutes.js
@@ -17,6 +17,13 @@ router.get('/check/:uid/:page', requireAuth, permissionController.checkAccess);
// 설정 가능 페이지 목록 (auth)
router.get('/available-pages', requireAuth, permissionController.getAvailablePages);
+// 부서별 권한 (admin)
+router.get('/departments/:deptId/permissions', requireAdmin, permissionController.getDepartmentPermissions);
+router.post('/departments/:deptId/bulk-set', requireAdmin, permissionController.bulkSetDepartmentPermissions);
+
+// 출처 포함 사용자 권한 조회 (admin or self)
+router.get('/users/:userId/effective-permissions', requireAuth, permissionController.getUserPermissionsWithSource);
+
// 권한 삭제 (admin)
router.delete('/:id', requireAdmin, permissionController.deletePermission);
diff --git a/user-management/migration-dept-permissions.sql b/user-management/migration-dept-permissions.sql
new file mode 100644
index 0000000..7539130
--- /dev/null
+++ b/user-management/migration-dept-permissions.sql
@@ -0,0 +1,19 @@
+-- 부서별 페이지 권한 관리 기능 DB 마이그레이션
+-- NAS SSH 접속 후 MariaDB 컨테이너에서 실행
+
+-- 1) sso_users에 department_id FK 추가
+ALTER TABLE sso_users
+ ADD COLUMN department_id INT NULL AFTER department,
+ ADD CONSTRAINT fk_sso_users_dept FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE SET NULL;
+
+-- 2) department_page_permissions 테이블 생성
+CREATE TABLE department_page_permissions (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ department_id INT NOT NULL,
+ page_name VARCHAR(50) NOT NULL,
+ can_access BOOLEAN DEFAULT FALSE,
+ granted_by_id INT NULL,
+ granted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY unique_dept_page (department_id, page_name),
+ FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE CASCADE
+);
diff --git a/user-management/web/index.html b/user-management/web/index.html
index 98f6ef8..1c65c35 100644
--- a/user-management/web/index.html
+++ b/user-management/web/index.html
@@ -7,7 +7,7 @@
-
+
@@ -46,6 +46,9 @@
+
@@ -86,13 +89,8 @@
-
@@ -118,66 +116,9 @@
-
-
-
-
페이지 접근 권한
-
-
-
-
-
-
-
-
-
-
-
- 공장관리
- System 1
-
-
-
- |
-
-
-
-
-
-
-
-
-
-
-
- 부적합관리
- System 3
-
-
-
- |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
사용자를 선택하면 권한을 설정할 수 있습니다
-
-