- {loading ? (
-
-
+ {/* 상세 패널 (우측) */}
+
+ {/* 이미지 */}
+
+
+
+
품목 이미지
- ) : (
-
-
-
- | SID |
- 분류 |
- 품명 |
- 모델 |
- 단가 |
- 공급처 |
- 제조사 |
-
-
-
- {filteredItems.map((item) => (
- handleRowClick(item)}
- className={clsx(
- 'hover:bg-white/10 transition-colors cursor-pointer',
- item.disable && 'opacity-50'
- )}
- >
- | {item.sid} |
- {item.cate} |
- {item.name} |
- {item.model} |
- {(item.price ?? 0).toLocaleString()} |
- {item.supply} |
- {item.manu} |
-
- ))}
- {filteredItems.length === 0 && (
-
- |
- {items.length === 0 ? '검색어를 입력하고 검색 버튼을 클릭하세요.' : '검색 결과가 없습니다.'}
- |
-
- )}
-
-
- )}
+
+ {detailLoading ? (
+
+ ) : itemImage ? (
+

+ ) : (
+
이미지 없음
+ )}
+
+
+
+ {/* 공급처 담당자 */}
+
+
+
+
+ {selectedItemDetail?.supply ? `[${selectedItemDetail.supply}] 담당자` : '공급처 담당자'}
+
+
+
+ {detailLoading ? (
+
+ ) : supplierStaff.length > 0 ? (
+
+
+
+ | 이름 |
+ 연락처 |
+ 이메일 |
+
+
+
+ {supplierStaff.map((staff) => (
+
+ | {staff.name} |
+ {staff.tel} |
+ {staff.email} |
+
+ ))}
+
+
+ ) : (
+
담당자 정보 없음
+ )}
+
+
+
+ {/* 최근 입고내역 */}
+
+
+
+
최근 입고내역
+
+
+ {detailLoading ? (
+
+ ) : incomingHistory.length > 0 ? (
+
+
+
+ | 일자 |
+ 요청자 |
+ 수량 |
+ 금액 |
+ 상태 |
+
+
+
+ {incomingHistory.map((h) => (
+
+ | {h.date} |
+ {h.request} |
+ {h.qty.toLocaleString()} |
+ {h.price.toLocaleString()} |
+ {h.state} |
+
+ ))}
+
+
+ ) : (
+
입고내역 없음
+ )}
+
+
+
+ {/* 발주내역 */}
+
+
+
+
발주내역
+
+
+ {detailLoading ? (
+
+ ) : orderHistory.length > 0 ? (
+
+
+
+ | 일자 |
+ 요청자 |
+ 수량 |
+ 금액 |
+ 상태 |
+
+
+
+ {orderHistory.map((h) => (
+
+ | {h.date} |
+ {h.request} |
+ {h.qty.toLocaleString()} |
+ {h.price.toLocaleString()} |
+ {h.state} |
+
+ ))}
+
+
+ ) : (
+
발주내역 없음
+ )}
+
+
diff --git a/Project/frontend/src/pages/Jobreport.tsx b/Project/frontend/src/pages/Jobreport.tsx
index 58d825f..5cd2204 100644
--- a/Project/frontend/src/pages/Jobreport.tsx
+++ b/Project/frontend/src/pages/Jobreport.tsx
@@ -29,7 +29,7 @@ export function Jobreport() {
// 페이징 상태
const [currentPage, setCurrentPage] = useState(1);
- const pageSize = 15;
+ const pageSize = 10;
// 권한 상태
const [canViewOT, setCanViewOT] = useState(false);
@@ -191,6 +191,7 @@ export function Jobreport() {
setFormData({
pdate: new Date().toISOString().split('T')[0], // 오늘 날짜
projectName: data.projectName || '',
+ pidx: data.pidx ?? null, // pidx도 복사
requestpart: data.requestpart || '',
package: data.package || '',
type: data.type || '',
@@ -220,6 +221,7 @@ export function Jobreport() {
setFormData({
pdate: data.pdate ? data.pdate.split('T')[0] : '',
projectName: data.projectName || '',
+ pidx: data.pidx ?? null,
requestpart: data.requestpart || '',
package: data.package || '',
type: data.type || '',
@@ -228,8 +230,8 @@ export function Jobreport() {
description: data.description || '',
hrs: data.hrs || 0,
ot: data.ot || 0,
- jobgrp: '', // 뷰에 없는 필드
- tag: '', // 뷰에 없는 필드
+ jobgrp: data.jobgrp || '',
+ tag: data.tag || '',
});
setShowModal(true);
}
@@ -264,6 +266,7 @@ export function Jobreport() {
itemIdx,
formData.pdate || '',
formData.projectName || '',
+ formData.pidx,
formData.requestpart || '',
formData.package || '',
formData.type || '',
@@ -279,6 +282,7 @@ export function Jobreport() {
response = await comms.addJobReport(
formData.pdate || '',
formData.projectName || '',
+ formData.pidx,
formData.requestpart || '',
formData.package || '',
formData.type || '',
diff --git a/Project/frontend/src/pages/Project.tsx b/Project/frontend/src/pages/Project.tsx
new file mode 100644
index 0000000..faed604
--- /dev/null
+++ b/Project/frontend/src/pages/Project.tsx
@@ -0,0 +1,457 @@
+import { useState, useEffect, useCallback } from 'react';
+import {
+ FolderKanban,
+ Search,
+ RefreshCw,
+ ChevronLeft,
+ ChevronRight,
+ User,
+ Calendar,
+ ExternalLink,
+} from 'lucide-react';
+import { comms } from '@/communication';
+import { ProjectListItem, ProjectListResponse } from '@/types';
+import { ProjectDetailDialog } from '@/components/project';
+
+// 상태별 색상 매핑
+const statusColors: Record
= {
+ 검토: { text: 'text-blue-400', bg: 'bg-blue-500/20' },
+ 진행: { text: 'text-green-400', bg: 'bg-green-500/20' },
+ 대기: { text: 'text-yellow-400', bg: 'bg-yellow-500/20' },
+ 보류: { text: 'text-orange-400', bg: 'bg-orange-500/20' },
+ 완료: { text: 'text-purple-400', bg: 'bg-purple-500/20' },
+ '완료(보고)': { text: 'text-gray-400', bg: 'bg-gray-500/20' },
+ 취소: { text: 'text-red-400', bg: 'bg-red-500/20' },
+};
+
+export function Project() {
+ const [projects, setProjects] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [selectedProject, setSelectedProject] = useState(null);
+ const [showDetailDialog, setShowDetailDialog] = useState(false);
+
+ // 필터 상태
+ const [categories, setCategories] = useState([]);
+ const [processes, setProcesses] = useState([]);
+ const [selectedCategory, setSelectedCategory] = useState('--전체--');
+ const [selectedProcess, setSelectedProcess] = useState('전체');
+ const [userFilter, setUserFilter] = useState('');
+ const [currentUserName, setCurrentUserName] = useState('');
+
+ // 상태 필터 체크박스
+ const [statusChecks, setStatusChecks] = useState({
+ 검토: true,
+ 진행: true,
+ 대기: true,
+ 보류: true,
+ 완료: false,
+ '완료(보고)': false,
+ 취소: false,
+ });
+
+ // 날짜 필터
+ const [dateType, setDateType] = useState('0');
+ const [yearStart, setYearStart] = useState(new Date().getFullYear().toString());
+
+ // 검색
+ const [searchKey, setSearchKey] = useState('');
+ const [filteredProjects, setFilteredProjects] = useState([]);
+
+ // 상태별 건수
+ const [statusCounts, setStatusCounts] = useState>({});
+
+ // 페이징
+ const [currentPage, setCurrentPage] = useState(1);
+ const pageSize = 20;
+
+ // 연도 목록 생성
+ const years = Array.from({ length: new Date().getFullYear() - 2009 }, (_, i) => (2010 + i).toString());
+
+ // 날짜 포맷
+ const formatDate = (dateStr?: string) => {
+ if (!dateStr) return '-';
+ const d = dateStr.substring(0, 10);
+ if (d.length < 10) return d;
+ return `${d.substring(2, 4)}-${d.substring(5, 7)}-${d.substring(8, 10)}`;
+ };
+
+ // 초기화
+ useEffect(() => {
+ const init = async () => {
+ // 현재 사용자 로드
+ try {
+ const loginStatus = await comms.checkLoginStatus();
+ if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) {
+ const userName = (loginStatus.User as { NameK?: string }).NameK || loginStatus.User.Name || '';
+ setCurrentUserName(userName);
+ setUserFilter(userName);
+ }
+ } catch (error) {
+ console.error('로그인 정보 로드 오류:', error);
+ }
+
+ // 분류/공정 목록 로드
+ try {
+ const [catRes, procRes] = await Promise.all([
+ comms.getProjectCategories(),
+ comms.getProjectProcesses(),
+ ]);
+ if (catRes.Success && catRes.Data) setCategories(catRes.Data as string[]);
+ if (procRes.Success && procRes.Data) setProcesses(procRes.Data as string[]);
+ } catch (error) {
+ console.error('필터 목록 로드 오류:', error);
+ }
+ };
+ init();
+ }, []);
+
+ // 데이터 로드
+ const loadProjects = useCallback(async () => {
+ setLoading(true);
+ try {
+ const checkedStatuses = Object.entries(statusChecks)
+ .filter(([, checked]) => checked)
+ .map(([status]) => status);
+
+ const statusFilter = checkedStatuses.length === 7 ? 'all' : checkedStatuses.join(',');
+
+ const response = await comms.getProjectList(
+ statusFilter,
+ selectedCategory,
+ selectedProcess,
+ userFilter,
+ dateType !== '0' ? yearStart : '',
+ dateType !== '0' ? yearStart : '',
+ dateType
+ ) as ProjectListResponse;
+
+ if (response.Success && response.Data) {
+ setProjects(response.Data);
+ setFilteredProjects(response.Data);
+ if (response.StatusCounts) {
+ setStatusCounts(response.StatusCounts as Record);
+ }
+ }
+ } catch (error) {
+ console.error('프로젝트 로드 오류:', error);
+ } finally {
+ setLoading(false);
+ }
+ }, [statusChecks, selectedCategory, selectedProcess, userFilter, dateType, yearStart]);
+
+ // 필터 변경 시 자동 조회
+ useEffect(() => {
+ if (currentUserName) {
+ loadProjects();
+ }
+ }, [loadProjects, currentUserName]);
+
+ // 검색어 필터링
+ useEffect(() => {
+ if (!searchKey.trim()) {
+ setFilteredProjects(projects);
+ } else {
+ const key = searchKey.toLowerCase();
+ const filtered = projects.filter(
+ (p) =>
+ p.name?.toLowerCase().includes(key) ||
+ p.userManager?.toLowerCase().includes(key) ||
+ p.usermain?.toLowerCase().includes(key) ||
+ p.orderno?.toLowerCase().includes(key) ||
+ p.memo?.toLowerCase().includes(key)
+ );
+ setFilteredProjects(filtered);
+ }
+ setCurrentPage(1);
+ }, [searchKey, projects]);
+
+ // 프로젝트 선택 시 다이얼로그 표시
+ const handleSelectProject = (project: ProjectListItem) => {
+ setSelectedProject(project);
+ setShowDetailDialog(true);
+ };
+
+ // 다이얼로그 닫기
+ const handleCloseDialog = () => {
+ setShowDetailDialog(false);
+ };
+
+ // 담당자 필터 토글
+ const toggleUserFilter = () => {
+ setUserFilter(userFilter ? '' : currentUserName);
+ };
+
+ // 상태 체크박스 토글
+ const toggleStatus = (status: string) => {
+ setStatusChecks((prev) => ({ ...prev, [status]: !prev[status as keyof typeof prev] }));
+ };
+
+ // 페이징 계산
+ const totalPages = Math.ceil(filteredProjects.length / pageSize);
+ const paginatedProjects = filteredProjects.slice(
+ (currentPage - 1) * pageSize,
+ currentPage * pageSize
+ );
+
+ // 자스민 링크 열기
+ const openJasmin = (jasminId?: number) => {
+ if (jasminId && jasminId > 0) {
+ window.open(`https://scwa.amkor.co.kr/jasmine/view/${jasminId}`, '_blank');
+ }
+ };
+
+ return (
+
+ {/* 헤더 */}
+
+
+
+
+
프로젝트 목록
+ ({filteredProjects.length}건)
+
+
+
+
+ {/* 필터 영역 */}
+
+ {/* 상태 필터 */}
+
+ {Object.entries(statusChecks).map(([status, checked]) => (
+
+ ))}
+
+
+ {/* 추가 필터 */}
+
+
+
+
+
+
+
+
+
+
+ {dateType !== '0' && (
+
+ )}
+
+
+
+
+ setSearchKey(e.target.value)}
+ placeholder="프로젝트명, 담당자..."
+ className="px-3 py-1.5 bg-white/5 border border-white/10 rounded-lg text-white text-sm w-48"
+ />
+
+
+
+
+
+ {/* 메인 콘텐츠 */}
+
+
+
+
+
+ | 상태 |
+ 프로젝트명 |
+ 담당 |
+ 요청자 |
+ 진행률 |
+ 시작 |
+ 만료/완료 |
+ |
+
+
+
+ {loading ? (
+
+ |
+
+ 로딩중...
+ |
+
+ ) : paginatedProjects.length === 0 ? (
+
+ |
+ 프로젝트가 없습니다.
+ |
+
+ ) : (
+ paginatedProjects.map((project) => {
+ const statusColor = statusColors[project.status] || { text: 'text-white', bg: 'bg-white/10' };
+ const isSelected = selectedProject?.idx === project.idx;
+ const rowBg = project.bHighlight
+ ? 'bg-lime-500/10'
+ : project.bCost
+ ? 'bg-yellow-500/10'
+ : project.bmajoritem
+ ? 'bg-pink-500/10'
+ : '';
+
+ return (
+ handleSelectProject(project)}
+ className={`cursor-pointer transition-colors ${rowBg} ${
+ isSelected ? 'bg-primary-500/20' : 'hover:bg-white/5'
+ }`}
+ >
+ |
+
+ {project.status}
+
+ |
+
+
+ {project.name}
+
+ |
+ {project.name_champion || project.userManager} |
+
+ {project.ReqLine}
+ {project.reqstaff}
+ |
+
+
+
+ {project.progress || 0}%
+
+ |
+ {formatDate(project.sdate)} |
+
+ {formatDate(project.ddate)}
+ {formatDate(project.edate)}
+ |
+
+ {project.jasmin && project.jasmin > 0 && (
+
+ )}
+ |
+
+ );
+ })
+ )}
+
+
+
+
+ {/* 페이징 */}
+ {totalPages > 1 && (
+
+
+
+ {currentPage} / {totalPages}
+
+
+
+ )}
+
+
+ {/* 프로젝트 상세 다이얼로그 */}
+ {showDetailDialog && selectedProject && (
+
+ )}
+
+ );
+}
diff --git a/Project/frontend/src/pages/UserAuth.tsx b/Project/frontend/src/pages/UserAuth.tsx
new file mode 100644
index 0000000..56b1364
--- /dev/null
+++ b/Project/frontend/src/pages/UserAuth.tsx
@@ -0,0 +1,502 @@
+import { useState, useEffect, useCallback } from 'react';
+import { Shield, Plus, Save, Trash2, Search, AlertCircle, RefreshCw } from 'lucide-react';
+import { comms } from '@/communication';
+import { AuthItem, AuthFieldInfo, GroupUser } from '@/types';
+import { UserSearchDialog } from '@/components/user';
+
+interface AuthFormData {
+ idx: number;
+ user: string;
+ account: number;
+ purchase: number;
+ purchaseEB: number;
+ holyday: number;
+ project: number;
+ jobreport: number;
+ scheapp: number;
+ equipment: number;
+ otconfirm: number;
+ holyreq: number;
+ kuntae: number;
+}
+
+const initialFormData: AuthFormData = {
+ idx: 0,
+ user: '',
+ account: 0,
+ purchase: 0,
+ purchaseEB: 0,
+ holyday: 0,
+ project: 0,
+ jobreport: 0,
+ scheapp: 0,
+ equipment: 0,
+ otconfirm: 0,
+ holyreq: 0,
+ kuntae: 0,
+};
+
+// 권한 필드 정보 (하드코딩 - API와 동일)
+const authFields: AuthFieldInfo[] = [
+ { field: 'user', label: '사용자 ID', description: '권한을 설정할 사용자 ID' },
+ { field: 'account', label: '계정', description: '계정 관리 권한' },
+ { field: 'purchase', label: '구매', description: '구매 관리 권한' },
+ { field: 'purchaseEB', label: '구매(전자실)', description: '전자실 구매 권한' },
+ { field: 'holyday', label: '출근부', description: '출근부 관리 권한' },
+ { field: 'project', label: '프로젝트', description: '프로젝트 관리 권한' },
+ { field: 'jobreport', label: '업무일지', description: '업무일지 관리 권한' },
+ { field: 'scheapp', label: '스케쥴', description: '스케쥴 관리 권한' },
+ { field: 'equipment', label: '장비목록', description: '장비 목록 관리 권한' },
+ { field: 'otconfirm', label: 'OT승인', description: '초과근무 승인 권한' },
+ { field: 'holyreq', label: '휴가요청', description: '휴가 요청 관리 권한' },
+ { field: 'kuntae', label: '근태', description: '근태 관리 권한' },
+];
+
+export default function UserAuth() {
+ const [loading, setLoading] = useState(true);
+ const [canAccess, setCanAccess] = useState(false);
+ const [accessMessage, setAccessMessage] = useState('');
+ const [authList, setAuthList] = useState([]);
+ const [filteredList, setFilteredList] = useState([]);
+ const [searchKey, setSearchKey] = useState('');
+ const [selectedItem, setSelectedItem] = useState(null);
+ const [formData, setFormData] = useState(initialFormData);
+ const [processing, setProcessing] = useState(false);
+ const [hasChanges, setHasChanges] = useState(false);
+ const [userNameMap, setUserNameMap] = useState>({});
+ const [showUserSearch, setShowUserSearch] = useState(false);
+
+ // 접근 권한 확인
+ const checkAccess = useCallback(async () => {
+ try {
+ const response = await comms.userAuthCanAccess();
+ if (response.Success) {
+ setCanAccess(response.CanAccess ?? false);
+ if (!response.CanAccess) {
+ setAccessMessage(response.Message || '(관리자/계정담당자) 전용 메뉴 입니다');
+ }
+ } else {
+ setCanAccess(false);
+ setAccessMessage(response.Message || '접근 권한 확인 실패');
+ }
+ } catch (error) {
+ console.error('접근 권한 확인 오류:', error);
+ setCanAccess(false);
+ setAccessMessage('접근 권한 확인 중 오류가 발생했습니다');
+ }
+ }, []);
+
+ // 사용자 이름 목록 로드
+ const loadUserNames = useCallback(async () => {
+ try {
+ const result = await comms.getUserList('%');
+ if (Array.isArray(result)) {
+ const nameMap: Record = {};
+ result.forEach((user: GroupUser) => {
+ nameMap[user.id] = user.name;
+ });
+ setUserNameMap(nameMap);
+ }
+ } catch (error) {
+ console.error('사용자 이름 목록 로드 오류:', error);
+ }
+ }, []);
+
+ // 목록 로드
+ const loadData = useCallback(async () => {
+ setLoading(true);
+ try {
+ const response = await comms.getUserAuthList();
+ if (response.Success && response.Data) {
+ setAuthList(response.Data);
+ setFilteredList(response.Data);
+ }
+ } catch (error) {
+ console.error('권한 목록 로드 오류:', error);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ // 초기 로드
+ useEffect(() => {
+ const init = async () => {
+ await checkAccess();
+ await loadUserNames();
+ await loadData();
+ };
+ init();
+ }, [checkAccess, loadUserNames, loadData]);
+
+ // 검색 필터링 (사번 또는 이름으로 검색)
+ useEffect(() => {
+ if (!searchKey.trim()) {
+ setFilteredList(authList);
+ } else {
+ const key = searchKey.toLowerCase();
+ setFilteredList(authList.filter(item =>
+ item.user.toLowerCase().includes(key) ||
+ (userNameMap[item.user] || '').toLowerCase().includes(key)
+ ));
+ }
+ }, [searchKey, authList, userNameMap]);
+
+ // 항목 선택
+ const handleSelectItem = (item: AuthItem) => {
+ if (hasChanges) {
+ if (!confirm('변경사항이 있습니다. 저장하지 않고 다른 항목을 선택하시겠습니까?')) {
+ return;
+ }
+ }
+ setSelectedItem(item);
+ setFormData({
+ idx: item.idx,
+ user: item.user,
+ account: item.account || 0,
+ purchase: item.purchase || 0,
+ purchaseEB: item.purchaseEB || 0,
+ holyday: item.holyday || 0,
+ project: item.project || 0,
+ jobreport: item.jobreport || 0,
+ scheapp: item.scheapp || 0,
+ equipment: item.equipment || 0,
+ otconfirm: item.otconfirm || 0,
+ holyreq: item.holyreq || 0,
+ kuntae: item.kuntae || 0,
+ });
+ setHasChanges(false);
+ };
+
+ // 새 항목 추가 - 사용자 검색 다이얼로그 열기
+ const handleAddNew = () => {
+ if (hasChanges) {
+ if (!confirm('변경사항이 있습니다. 저장하지 않고 새 항목을 추가하시겠습니까?')) {
+ return;
+ }
+ }
+ setShowUserSearch(true);
+ };
+
+ // 사용자 검색에서 선택 시
+ const handleUserSelected = async (user: GroupUser) => {
+ // 이미 등록된 사용자인지 확인
+ const existingItem = authList.find(item => item.user === user.id);
+ if (existingItem) {
+ // 이미 등록되어 있으면 해당 항목 선택
+ handleSelectItem(existingItem);
+ return;
+ }
+
+ // 새 사용자 권한 추가
+ setProcessing(true);
+ try {
+ const response = await comms.saveUserAuth(
+ 0, // 새 항목
+ user.id,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 모든 권한 0으로 초기화
+ );
+
+ if (response.Success) {
+ // 목록 새로고침
+ await loadData();
+ // 새로 추가된 항목 선택
+ const data = response.Data as { idx?: number } | undefined;
+ const newIdx = data?.idx;
+ if (newIdx) {
+ // 잠시 후 새 항목 선택 (loadData가 완료된 후)
+ setTimeout(() => {
+ const newItem = authList.find(item => item.user === user.id);
+ if (newItem) {
+ handleSelectItem(newItem);
+ } else {
+ // authList가 아직 업데이트되지 않았을 수 있으므로 폼 데이터 직접 설정
+ setSelectedItem(null);
+ setFormData({
+ idx: newIdx,
+ user: user.id,
+ account: 0, purchase: 0, purchaseEB: 0, holyday: 0,
+ project: 0, jobreport: 0, scheapp: 0, equipment: 0,
+ otconfirm: 0, holyreq: 0, kuntae: 0,
+ });
+ setHasChanges(false);
+ }
+ }, 100);
+ }
+ } else {
+ alert(response.Message || '사용자 추가에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('사용자 추가 오류:', error);
+ alert('사용자 추가 중 오류가 발생했습니다.');
+ } finally {
+ setProcessing(false);
+ }
+ };
+
+ // 필드 변경
+ const handleFieldChange = (field: keyof AuthFormData, value: string | number) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ setHasChanges(true);
+ };
+
+ // 저장
+ const handleSave = async () => {
+ if (!formData.user.trim()) {
+ alert('사용자 ID를 입력하세요.');
+ return;
+ }
+
+ setProcessing(true);
+ try {
+ const response = await comms.saveUserAuth(
+ formData.idx,
+ formData.user,
+ formData.account,
+ formData.purchase,
+ formData.purchaseEB,
+ formData.holyday,
+ formData.project,
+ formData.jobreport,
+ formData.scheapp,
+ formData.equipment,
+ formData.otconfirm,
+ formData.holyreq,
+ formData.kuntae
+ );
+
+ if (response.Success) {
+ setHasChanges(false);
+ await loadData();
+ // 새로 추가한 경우 해당 항목 선택
+ const data = response.Data as { idx?: number } | undefined;
+ if (formData.idx === 0 && data?.idx) {
+ const newIdx = data.idx;
+ const newItem = authList.find(item => item.idx === newIdx);
+ if (newItem) {
+ setSelectedItem(newItem);
+ setFormData(prev => ({ ...prev, idx: newIdx }));
+ }
+ }
+ } else {
+ alert(response.Message || '저장에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('저장 오류:', error);
+ alert('저장 중 오류가 발생했습니다.');
+ } finally {
+ setProcessing(false);
+ }
+ };
+
+ // 삭제
+ const handleDelete = async () => {
+ if (!selectedItem) return;
+
+ if (!confirm(`"${selectedItem.user}" 사용자의 권한 설정을 삭제하시겠습니까?`)) {
+ return;
+ }
+
+ setProcessing(true);
+ try {
+ const response = await comms.deleteUserAuth(selectedItem.idx);
+ if (response.Success) {
+ setSelectedItem(null);
+ setFormData(initialFormData);
+ setHasChanges(false);
+ await loadData();
+ } else {
+ alert(response.Message || '삭제에 실패했습니다.');
+ }
+ } catch (error) {
+ console.error('삭제 오류:', error);
+ alert('삭제 중 오류가 발생했습니다.');
+ } finally {
+ setProcessing(false);
+ }
+ };
+
+ // 접근 불가 화면
+ if (!canAccess && !loading) {
+ return (
+
+
+
+
접근 권한 없음
+
{accessMessage}
+
+
+ );
+ }
+
+ return (
+ <>
+
+ {/* 헤더 */}
+
+
+
+
+
+
+
사용자 권한
+
사용자별 기능 접근 권한 설정
+
+
+
+
+
+
+
+ {/* 메인 컨텐츠 */}
+
+ {/* 좌측: 사용자 목록 */}
+
+ {/* 검색 */}
+
+
+ setSearchKey(e.target.value)}
+ placeholder="사번/이름 검색..."
+ className="w-full pl-10 pr-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
+ />
+
+
+ {/* 추가 버튼 */}
+
+
+ {/* 목록 */}
+
+ {loading ? (
+
+ ) : filteredList.length === 0 ? (
+
+ {searchKey ? '검색 결과가 없습니다' : '등록된 사용자가 없습니다'}
+
+ ) : (
+ filteredList.map(item => (
+
+ ))
+ )}
+
+
+ {/* 항목 수 */}
+
+ 총 {filteredList.length}명
+
+
+
+ {/* 우측: 상세 편집 */}
+
+ {formData.idx === 0 && !formData.user ? (
+
+
+
+
좌측에서 사용자를 선택하거나
+
새 사용자를 추가하세요
+
+
+ ) : (
+
+ {/* 권한 설정 그리드 */}
+
+
0: 권한 없음, 1~9: 레벨 (숫자가 높을수록 높은 권한)
+
+
+ {authFields.filter(f => f.field !== 'user').map(field => (
+
+
+
handleFieldChange(field.field as keyof AuthFormData, parseInt(e.target.value) || 0)}
+ className="w-full bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white text-center focus:outline-none focus:ring-2 focus:ring-primary-400"
+ />
+
+ {field.description}
+
+
+ ))}
+
+
+
+ {/* 버튼 영역 */}
+
+
+ {selectedItem && (
+
+ )}
+
+
+
+
+ )}
+
+
+
+
+ {/* 사용자 검색 다이얼로그 */}
+ setShowUserSearch(false)}
+ onSelect={handleUserSelected}
+ title="사용자 선택"
+ excludeUsers={authList.map(item => item.user)}
+ initialSearchKey={searchKey}
+ />
+ >
+ );
+}
diff --git a/Project/frontend/src/pages/index.ts b/Project/frontend/src/pages/index.ts
index 5e01bcc..43c36c6 100644
--- a/Project/frontend/src/pages/index.ts
+++ b/Project/frontend/src/pages/index.ts
@@ -2,6 +2,7 @@ export { Dashboard } from './Dashboard';
export { Todo } from './Todo';
export { Kuntae } from './Kuntae';
export { Jobreport } from './Jobreport';
+export { Project } from './Project';
export { PlaceholderPage } from './Placeholder';
export { Login } from './Login';
export { CommonCodePage } from './CommonCode';
@@ -10,3 +11,4 @@ export { UserListPage } from './UserList';
export { MonthlyWorkPage } from './MonthlyWork';
export { MailFormPage } from './MailForm';
export { UserGroupPage } from './UserGroup';
+export { default as UserAuthPage } from './UserAuth';
diff --git a/Project/frontend/src/types.ts b/Project/frontend/src/types.ts
index 01c128e..90e2aeb 100644
--- a/Project/frontend/src/types.ts
+++ b/Project/frontend/src/types.ts
@@ -123,7 +123,7 @@ export interface JobreportModel {
wdate: string;
}
-// 업무일지 타입 (vJobReportForUser 뷰)
+// 업무일지 타입 (vJobReportForUser 뷰 + JobReport 테이블)
export interface JobReportItem {
idx: number;
pidx: number;
@@ -143,6 +143,8 @@ export interface JobReportItem {
ww: string;
otpms: string; // OT PMS
process: string;
+ jobgrp?: string; // 업무분류 (상세 조회 시)
+ tag?: string; // 태그 (상세 조회 시)
}
// 업무일지 사용자 타입
@@ -243,12 +245,37 @@ export interface ItemInfo {
unit: string;
price: number;
supply: string;
+ supplyidx?: number;
manu: string;
storage: string;
disable: boolean;
memo: string;
}
+// 품목 상세 정보 (supplyidx 포함)
+export interface ItemDetail extends ItemInfo {
+ supplyidx: number;
+}
+
+// 공급처 담당자 타입
+export interface SupplierStaff {
+ idx: number;
+ name: string;
+ tel: string;
+ email: string;
+ dept: string;
+}
+
+// 구매내역 항목 타입 (입고/발주)
+export interface PurchaseHistoryItem {
+ idx: number;
+ date: string;
+ request: string;
+ qty: number;
+ price: number;
+ state: string;
+}
+
// 사용자 목록 관련 타입
export interface GroupUser {
id: string;
@@ -295,13 +322,19 @@ export interface MachineBridgeInterface {
// Kuntae API
Kuntae_GetList(sd: string, ed: string): Promise;
Kuntae_Delete(id: number): Promise;
+ Kuntae_ErrorCheck(sd: string, ed: string): Promise;
+ Kuntae_FixError(pdate: string): Promise;
+ Kuntae_FixErrors(dates: string): Promise;
+
+ // Favorite API
+ Favorite_GetList(): Promise;
// Jobreport API (JobReport 뷰/테이블)
Jobreport_GetList(sd: string, ed: string, uid: string, cate: string, searchKey: string): Promise;
Jobreport_GetUsers(): Promise;
Jobreport_GetDetail(id: number): Promise;
- Jobreport_Add(pdate: string, projectName: string, requestpart: string, package_: string, type: string, process: string, status: string, description: string, hrs: number, ot: number, jobgrp: string, tag: string): Promise;
- Jobreport_Edit(idx: number, pdate: string, projectName: string, requestpart: string, package_: string, type: string, process: string, status: string, description: string, hrs: number, ot: number, jobgrp: string, tag: string): Promise;
+ Jobreport_Add(pdate: string, projectName: string, pidx: number, requestpart: string, package_: string, type: string, process: string, status: string, description: string, hrs: number, ot: number, jobgrp: string, tag: string): Promise;
+ Jobreport_Edit(idx: number, pdate: string, projectName: string, pidx: number, requestpart: string, package_: string, type: string, process: string, status: string, description: string, hrs: number, ot: number, jobgrp: string, tag: string): Promise;
Jobreport_Delete(id: number): Promise;
Jobreport_GetPermission(targetUserId: string): Promise;
Jobreport_GetJobTypes(process: string): Promise;
@@ -333,6 +366,13 @@ export interface MachineBridgeInterface {
Items_GetList(category: string, searchKey: string): Promise;
Items_Save(idx: number, sid: string, cate: string, name: string, model: string, scale: string, unit: string, price: number, supply: string, manu: string, storage: string, disable: boolean, memo: string): Promise;
Items_Delete(idx: number): Promise;
+ Items_GetImage(idx: number): Promise;
+ Items_SaveImage(idx: number, base64Image: string): Promise;
+ Items_DeleteImage(idx: number): Promise;
+ Items_GetDetail(idx: number): Promise;
+ Items_GetSupplierStaff(supplyIdx: number): Promise;
+ Items_GetIncomingHistory(itemIdx: number): Promise;
+ Items_GetOrderHistory(itemIdx: number): Promise;
// UserList API
UserList_GetCurrentLevel(): Promise;
@@ -360,6 +400,28 @@ export interface MachineBridgeInterface {
UserGroup_Edit(originalDept: string, dept: string, path_kj: string, permission: number, advpurchase: boolean, advkisul: boolean, managerinfo: string, devinfo: string, usemail: boolean): Promise;
UserGroup_Delete(dept: string): Promise;
UserGroup_GetPermissionInfo(): Promise;
+
+ // UserAuth API (사용자 권한)
+ UserAuth_CanAccess(): Promise;
+ UserAuth_GetList(): Promise;
+ UserAuth_Save(idx: number, user: string, account: number, purchase: number, purchaseEB: number, holyday: number, project: number, jobreport: number, scheapp: number, equipment: number, otconfirm: number, holyreq: number, kuntae: number): Promise;
+ UserAuth_Delete(idx: number): Promise;
+ UserAuth_GetFields(): Promise;
+
+ // 범용 권한 체크 API
+ CheckAuth(authType: string, requiredLevel: number): Promise;
+ GetMyAuth(): Promise;
+
+ // 프로젝트 검색 API (업무일지용)
+ Project_Search(keyword: string): Promise;
+ Project_GetUserProjects(): Promise;
+
+ // 프로젝트 목록 API
+ Project_GetCategories(): Promise;
+ Project_GetProcesses(): Promise;
+ Project_GetList(statusFilter: string, category: string, process: string, userFilter: string, yearStart: string, yearEnd: string, dateType: string): Promise;
+ Project_GetHistory(projectIdx: number): Promise;
+ Project_GetDailyMemo(projectIdx: number): Promise;
}
// 사용자 권한 정보 타입
@@ -465,3 +527,217 @@ export interface PermissionInfo {
label: string;
description: string;
}
+
+// 사용자 권한 항목 타입 (Auth 테이블)
+export interface AuthItem {
+ idx: number;
+ user: string;
+ gcode?: string;
+ account: number;
+ purchase: number;
+ purchaseEB: number;
+ holyday: number;
+ project: number;
+ jobreport: number;
+ scheapp: number;
+ equipment: number;
+ otconfirm: number;
+ holyreq: number;
+ kuntae: number;
+}
+
+// 사용자 권한 필드 정보
+export interface AuthFieldInfo {
+ field: string;
+ label: string;
+ description: string;
+}
+
+// 범용 권한 체크 응답 타입
+export interface CheckAuthResponse {
+ Success: boolean;
+ CanAccess?: boolean;
+ UserLevel?: number;
+ AuthLevel?: number;
+ EffectiveLevel?: number;
+ RequiredLevel?: number;
+ AuthType?: string;
+ Message?: string;
+}
+
+// 내 권한 정보 응답 타입
+export interface MyAuthInfo {
+ UserLevel: number;
+ account: number;
+ purchase: number;
+ purchaseEB: number;
+ holyday: number;
+ project: number;
+ jobreport: number;
+ scheapp: number;
+ equipment: number;
+ otconfirm: number;
+ holyreq: number;
+ kuntae: number;
+}
+
+// 권한 타입 (FCOMMON DBM.eAuthType과 일치)
+export type AuthType = 'purchase' | 'holyday' | 'project' | 'jobreport' | 'savecost' | 'equipment' | 'otconfirm' | 'kuntae' | 'holyreq' | 'account' | 'purchaseEB' | 'scheapp';
+
+// 프로젝트 검색 결과 항목 타입
+export interface ProjectSearchItem {
+ idx: number;
+ name: string;
+ userManager: string;
+ userMain: string;
+ status: string;
+ source: 'project' | 'jobreport';
+ lastDate?: string;
+}
+
+// 근태 오류검사 결과 항목 타입
+export interface KuntaeErrorCheckResult {
+ Date: string;
+ Gubun: string;
+ OccurDay: string;
+ OccurTime: string;
+ UseDay: string;
+ UseTime: string;
+ CateError: string;
+ IsError: boolean;
+ IsMagam: boolean;
+}
+
+// 근태 오류검사 응답 타입
+export interface KuntaeErrorCheckResponse {
+ Success: boolean;
+ Message?: string;
+ OkList?: KuntaeErrorCheckResult[];
+ NgList?: KuntaeErrorCheckResult[];
+}
+
+// 근태 오류수정 결과 타입
+export interface KuntaeFixErrorResult {
+ Date: string;
+ Success: boolean;
+ Message: string;
+}
+
+// 즐겨찾기 아이템 타입
+export interface FavoriteItem {
+ name: string;
+ url: string;
+}
+
+// 프로젝트 목록 아이템 타입
+export interface ProjectListItem {
+ idx: number;
+ pidx?: number;
+ status: string;
+ asset?: string;
+ level?: number;
+ rev?: number;
+ process?: string;
+ part?: string;
+ pdate?: string;
+ name: string;
+ userManager?: string;
+ usermain?: string;
+ usersub?: string;
+ userhw2?: string;
+ reqstaff?: string;
+ costo?: number;
+ costn?: number;
+ cnt?: number;
+ remark_req?: string;
+ remark_ans?: string;
+ sdate?: string;
+ ddate?: string;
+ edate?: string;
+ odate?: string;
+ progress?: number;
+ bmajoritem?: boolean;
+ memo?: string;
+ wuid?: number;
+ wdate?: string;
+ orderno?: string;
+ crdue?: string;
+ import?: string;
+ path?: string;
+ userprocess?: string;
+ bCost?: boolean;
+ bFanOut?: boolean;
+ bHighlight?: boolean;
+ div?: string;
+ model?: string;
+ serial?: string;
+ championid?: number;
+ designid?: number;
+ epanelid?: number;
+ softwareid?: number;
+ effect_tangible?: string;
+ effect_intangible?: string;
+ name_champion?: string;
+ name_design?: string;
+ name_epanel?: string;
+ name_software?: string;
+ category?: string;
+ ReqLine?: string;
+ ReqSite?: string;
+ ReqPackage?: string;
+ ReqPlant?: string;
+ pno?: number;
+ kdate?: string;
+ jasmin?: number;
+ sfi?: string;
+ lasthistory_date?: string;
+ lastSchNo?: number;
+ cramount?: number;
+ panelimage?: string;
+ Priority?: number;
+ sfi_count?: number;
+ ProgressPrj?: number;
+ finishrate?: number;
+}
+
+// 프로젝트 목록 응답 타입
+export interface ProjectListResponse {
+ Success: boolean;
+ Message?: string;
+ Data?: ProjectListItem[];
+ StatusCounts?: {
+ 검토: number;
+ 진행: number;
+ 대기: number;
+ 보류: number;
+ 완료: number;
+ '완료(보고)': number;
+ 취소: number;
+ };
+ TotalCosto?: number;
+ TotalCostn?: number;
+ CurrentUser?: string;
+}
+
+// 프로젝트 히스토리 타입
+export interface ProjectHistory {
+ idx: number;
+ pidx: number;
+ pdate: string;
+ progress?: number;
+ remark?: string;
+ wuid?: number;
+ wdate?: string;
+ wname?: string;
+}
+
+// 프로젝트 일일 메모 타입
+export interface ProjectDailyMemo {
+ idx: number;
+ pidx: number;
+ pdate: string;
+ remark?: string;
+ wuid?: number;
+ wdate?: string;
+ wname?: string;
+}
diff --git a/Project/frontend/vite.config.ts b/Project/frontend/vite.config.ts
index aafd7a6..557a742 100644
--- a/Project/frontend/vite.config.ts
+++ b/Project/frontend/vite.config.ts
@@ -20,6 +20,14 @@ export default defineConfig({
build: {
outDir: 'dist',
emptyOutDir: true,
+ rollupOptions: {
+ output: {
+ // 파일명에서 해시 제거 - 고정된 파일명 사용
+ entryFileNames: 'assets/[name].js',
+ chunkFileNames: 'assets/[name].js',
+ assetFileNames: 'assets/[name].[ext]',
+ },
+ },
},
base: './',
});