diff --git a/Project/EETGW.csproj b/Project/EETGW.csproj index 5cff649..96d5753 100644 --- a/Project/EETGW.csproj +++ b/Project/EETGW.csproj @@ -367,6 +367,7 @@ + diff --git a/Project/EETGW.csproj.user b/Project/EETGW.csproj.user index ca2fa97..0789c32 100644 --- a/Project/EETGW.csproj.user +++ b/Project/EETGW.csproj.user @@ -9,7 +9,7 @@ ko-KR false - ShowAllFiles + ProjectFiles false diff --git a/Project/Properties/AssemblyInfo.cs b/Project/Properties/AssemblyInfo.cs index 6e7d7d5..ff35ac3 100644 --- a/Project/Properties/AssemblyInfo.cs +++ b/Project/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호가 자동으로 // 지정되도록 할 수 있습니다. // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("25.11.11.1030")] -[assembly: AssemblyFileVersion("25.11.11.1030")] +[assembly: AssemblyVersion("25.12.12.1050")] +[assembly: AssemblyFileVersion("25.12.12.1050")] diff --git a/Project/Web/MachineBridge/MachineBridge.HolidayRequest.cs b/Project/Web/MachineBridge/MachineBridge.HolidayRequest.cs new file mode 100644 index 0000000..29fe1ec --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.HolidayRequest.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using Newtonsoft.Json; +using FCOMMON; + +namespace Project.Web +{ + public partial class MachineBridge + { + #region HolidayRequest API (휴가/외출 신청) + + /// + /// 휴가/외출 신청 목록 조회 + /// + public string HolidayRequest_GetList(string startDate, string endDate, string userId, int userLevel) + { + try + { + // 권한에 따른 uid 필터링 + // userLevel < 5: 본인만 조회 + // userLevel >= 5: userId가 '%'이면 전체, 특정 uid면 해당 사용자만 + var uidFilter = userLevel < 5 ? info.Login.no : (string.IsNullOrEmpty(userId) || userId == "%" ? "%" : userId); + + var sql = @" + SELECT + hr.idx, hr.gcode, hr.uid, hr.cate, hr.sdate, hr.edate, hr.Remark, hr.wuid, hr.wdate, + u.dept, u.name, u.grade, u.tel, u.processs, + hr.Response, hr.conf, hr.HolyReason, hr.HolyBackup, hr.HolyLocation, + hr.HolyDays, hr.HolyTimes, hr.sendmail, hr.stime, hr.etime, hr.conf_id, hr.conf_time + FROM EETGW_HolydayRequest hr WITH (nolock) + LEFT OUTER JOIN vGroupUser u ON hr.uid = u.id AND hr.gcode = u.gcode + WHERE hr.gcode = @gcode + AND hr.sdate >= @startDate + AND hr.sdate <= @endDate + AND hr.uid LIKE @uid + ORDER BY hr.conf, hr.sdate DESC"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@startDate", startDate); + cmd.Parameters.AddWithValue("@endDate", endDate); + cmd.Parameters.AddWithValue("@uid", uidFilter); + + using (var da = new SqlDataAdapter(cmd)) + { + var dt = new DataTable(); + da.Fill(dt); + + // 승인/미승인 합계 계산 + decimal sumApprovedDays = 0; + decimal sumApprovedTimes = 0; + decimal sumPendingDays = 0; + decimal sumPendingTimes = 0; + + foreach (DataRow row in dt.Rows) + { + var conf = Convert.ToInt32(row["conf"]); + var days = row["HolyDays"] != DBNull.Value ? Convert.ToDecimal(row["HolyDays"]) : 0; + var times = row["HolyTimes"] != DBNull.Value ? Convert.ToDecimal(row["HolyTimes"]) : 0; + + if (conf == 1) + { + sumApprovedDays += days; + sumApprovedTimes += times; + } + else + { + sumPendingDays += days; + sumPendingTimes += times; + } + } + + return JsonConvert.SerializeObject(new + { + Success = true, + Data = dt, + Summary = new + { + ApprovedDays = sumApprovedDays, + ApprovedTimes = sumApprovedTimes, + PendingDays = sumPendingDays, + PendingTimes = sumPendingTimes + } + }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 휴가/외출 신청 저장 + /// + public string HolidayRequest_Save(int idx, string uid, string cate, string sdate, string edate, + string remark, string response, int conf, string holyReason, string holyBackup, + string holyLocation, decimal holyDays, decimal holyTimes, string stime, string etime) + { + try + { + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + { + cn.Open(); + var sql = ""; + + if (idx == 0) // INSERT + { + sql = @"INSERT INTO EETGW_HolydayRequest + (gcode, uid, cate, sdate, edate, conf, Remark, wuid, wdate, Response, + HolyReason, HolyBackup, HolyLocation, HolyDays, HolyTimes, stime, etime) + VALUES + (@gcode, @uid, @cate, @sdate, @edate, @conf, @remark, @wuid, GETDATE(), @response, + @holyReason, @holyBackup, @holyLocation, @holyDays, @holyTimes, @stime, @etime)"; + } + else // UPDATE + { + sql = @"UPDATE EETGW_HolydayRequest + SET uid = @uid, cate = @cate, sdate = @sdate, edate = @edate, conf = @conf, + Remark = @remark, Response = @response, HolyReason = @holyReason, + HolyBackup = @holyBackup, HolyLocation = @holyLocation, + HolyDays = @holyDays, HolyTimes = @holyTimes, stime = @stime, etime = @etime + WHERE idx = @idx AND gcode = @gcode"; + } + + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@uid", uid); + cmd.Parameters.AddWithValue("@cate", cate); + cmd.Parameters.AddWithValue("@sdate", sdate); + cmd.Parameters.AddWithValue("@edate", edate); + cmd.Parameters.AddWithValue("@conf", conf); + cmd.Parameters.AddWithValue("@remark", remark ?? ""); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); // 작성자 + cmd.Parameters.AddWithValue("@response", response ?? ""); + cmd.Parameters.AddWithValue("@holyReason", holyReason ?? ""); + cmd.Parameters.AddWithValue("@holyBackup", holyBackup ?? ""); + cmd.Parameters.AddWithValue("@holyLocation", holyLocation ?? ""); + cmd.Parameters.AddWithValue("@holyDays", holyDays); + cmd.Parameters.AddWithValue("@holyTimes", holyTimes); + cmd.Parameters.AddWithValue("@stime", stime ?? ""); + cmd.Parameters.AddWithValue("@etime", etime ?? ""); + + cmd.ExecuteNonQuery(); + } + } + return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다." }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + #endregion + } +} diff --git a/Project/Web/MachineBridge/WebSocketServer.cs b/Project/Web/MachineBridge/WebSocketServer.cs index 7826e7f..c0b3b5f 100644 --- a/Project/Web/MachineBridge/WebSocketServer.cs +++ b/Project/Web/MachineBridge/WebSocketServer.cs @@ -599,6 +599,7 @@ namespace Project.Web } break; + // ===== JobReport API (JobReport 뷰/테이블) ===== case "JOBREPORT_GET_LIST": { @@ -607,9 +608,10 @@ namespace Project.Web string uid = json.uid ?? ""; string cate = json.cate ?? ""; // 사용안함 (호환성) string searchKey = json.searchKey ?? ""; + string requestId = json.requestId; Console.WriteLine($"[WS] JOBREPORT_GET_LIST: sd={sd}, ed={ed}, uid={uid}, searchKey={searchKey}"); string result = _bridge.Jobreport_GetList(sd, ed, uid, cate, searchKey); - var response = new { type = "JOBREPORT_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + var response = new { type = "JOBREPORT_LIST_DATA", data = JsonConvert.DeserializeObject(result), requestId = requestId }; await Send(socket, JsonConvert.SerializeObject(response)); } break; @@ -1199,6 +1201,43 @@ namespace Project.Web } break; + // ===== HolidayRequest API (휴가/외출 신청) ===== + case "HOLIDAY_REQUEST_GET_LIST": + { + string startDate = json.startDate ?? ""; + string endDate = json.endDate ?? ""; + string userId = json.userId ?? ""; + int userLevel = json.userLevel ?? 0; + string result = _bridge.HolidayRequest_GetList(startDate, endDate, userId, userLevel); + var response = new { type = "HOLIDAY_REQUEST_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "HOLIDAY_REQUEST_SAVE": + { + int idx = json.idx ?? 0; + string uid = json.uid ?? ""; + string cate = json.cate ?? ""; + string sdate = json.sdate ?? ""; + string edate = json.edate ?? ""; + string remark = json.remark ?? ""; + string responseMsg = json.response ?? ""; + int conf = json.conf ?? 0; + string holyReason = json.holyReason ?? ""; + string holyBackup = json.holyBackup ?? ""; + string holyLocation = json.holyLocation ?? ""; + decimal holyDays = json.holyDays ?? 0; + decimal holyTimes = json.holyTimes ?? 0; + string stime = json.stime ?? ""; + string etime = json.etime ?? ""; + + string result = _bridge.HolidayRequest_Save(idx, uid, cate, sdate, edate, remark, responseMsg, conf, holyReason, holyBackup, holyLocation, holyDays, holyTimes, stime, etime); + var response = new { type = "HOLIDAY_REQUEST_SAVED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + // ===== MailForm API (메일양식) ===== case "MAILFORM_GET_LIST": { diff --git a/Project/frontend/src/App.tsx b/Project/frontend/src/App.tsx index e1c4435..245a26c 100644 --- a/Project/frontend/src/App.tsx +++ b/Project/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { MailList } from '@/pages/MailList'; import { Customs } from '@/pages/Customs'; import { LicenseList } from '@/components/license/LicenseList'; import { PartList } from '@/pages/PartList'; +import HolidayRequest from '@/pages/HolidayRequest'; import { comms } from '@/communication'; import { UserInfo } from '@/types'; import { Loader2 } from 'lucide-react'; @@ -90,6 +91,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/Project/frontend/src/communication.ts b/Project/frontend/src/communication.ts index 3ab48ce..d059721 100644 --- a/Project/frontend/src/communication.ts +++ b/Project/frontend/src/communication.ts @@ -44,6 +44,7 @@ import type { CustomItem, LicenseItem, PartListItem, + HolidayRequest, } from '@/types'; // WebView2 환경 감지 @@ -137,21 +138,28 @@ class CommunicationLayer { }, 2000); } + const requestId = Date.now().toString() + Math.random().toString(36).substr(2, 9); + const timeoutId = setTimeout(() => { this.listeners = this.listeners.filter(cb => cb !== handler); reject(new Error(`${requestType} timeout`)); }, 10000); const handler = (data: unknown) => { - const msg = data as { type: string; data?: T; Success?: boolean; Message?: string }; + const msg = data as { type: string; data?: T; Success?: boolean; Message?: string; requestId?: string }; if (msg.type === responseType) { + // requestId가 있는 경우 일치 여부 확인 (백엔드가 지원하는 경우) + if (msg.requestId && msg.requestId !== requestId) { + return; + } + clearTimeout(timeoutId); this.listeners = this.listeners.filter(cb => cb !== handler); resolve(msg.data as T); } }; this.listeners.push(handler); - this.ws?.send(JSON.stringify({ ...params, type: requestType })); + this.ws?.send(JSON.stringify({ ...params, type: requestType, requestId })); }); } @@ -916,6 +924,39 @@ class CommunicationLayer { } } + // ===== HolidayRequest API (휴가/외출 신청) ===== + + public async getHolidayRequestList(startDate: string, endDate: string, userId: string, userLevel: number): Promise> { + if (isWebView && machine) { + const result = await machine.HolidayRequest_GetList(startDate, endDate, userId, userLevel); + return JSON.parse(result); + } else { + return this.wsRequest>('HOLIDAY_REQUEST_GET_LIST', 'HOLIDAY_REQUEST_LIST_DATA', { startDate, endDate, userId, userLevel }); + } + } + + public async saveHolidayRequest(data: HolidayRequest): Promise { + const { idx, uid, cate, sdate, edate, Remark, Response, conf, HolyReason, HolyBackup, HolyLocation, HolyDays, HolyTimes, stime, etime } = data; + const remark = Remark || ''; + const response = Response || ''; + const holyReason = HolyReason || ''; + const holyBackup = HolyBackup || ''; + const holyLocation = HolyLocation || ''; + const holyDays = HolyDays || 0; + const holyTimes = HolyTimes || 0; + const sTime = stime || ''; + const eTime = etime || ''; + + if (isWebView && machine) { + const result = await machine.HolidayRequest_Save(idx, uid, cate, sdate, edate, remark, response, conf, holyReason, holyBackup, holyLocation, holyDays, holyTimes, sTime, eTime); + return JSON.parse(result); + } else { + return this.wsRequest('HOLIDAY_REQUEST_SAVE', 'HOLIDAY_REQUEST_SAVED', { + idx, uid, cate, sdate, edate, remark, response, conf, holyReason, holyBackup, holyLocation, holyDays, holyTimes, stime: sTime, etime: eTime + }); + } + } + // ===== MailForm API (메일양식) ===== public async getMailFormList(): Promise> { diff --git a/Project/frontend/src/components/holiday/HolidayRequestDialog.tsx b/Project/frontend/src/components/holiday/HolidayRequestDialog.tsx new file mode 100644 index 0000000..5de625c --- /dev/null +++ b/Project/frontend/src/components/holiday/HolidayRequestDialog.tsx @@ -0,0 +1,507 @@ +import { useState, useEffect } from 'react'; +import { X, Save, Calendar, Clock, MapPin, User, FileText, AlertCircle } from 'lucide-react'; +import { comms } from '@/communication'; +import { HolidayRequest, CommonCode } from '@/types'; + +interface HolidayRequestDialogProps { + isOpen: boolean; + onClose: () => void; + onSave: () => void; + initialData?: HolidayRequest | null; + userLevel: number; + currentUserId: string; + currentUserName: string; +} + +export function HolidayRequestDialog({ + isOpen, + onClose, + onSave, + initialData, + userLevel, + currentUserId, + // currentUserName +}: HolidayRequestDialogProps) { + const [loading, setLoading] = useState(false); + const [codes, setCodes] = useState<{ + cate: CommonCode[]; + reason: CommonCode[]; + location: CommonCode[]; + backup: CommonCode[]; + }>({ + cate: [], + reason: [], + location: [], + backup: [] + }); + const [users, setUsers] = useState>([]); + + // Form State + const [formData, setFormData] = useState({ + idx: 0, + gcode: '', + uid: currentUserId, + cate: '', + sdate: new Date().toISOString().split('T')[0], + edate: new Date().toISOString().split('T')[0], + Remark: '', + wuid: currentUserId, + wdate: '', + Response: '', + conf: 0, + HolyReason: '', + HolyBackup: '', + HolyLocation: '', + HolyDays: 0, + HolyTimes: 0, + stime: '09:00', + etime: '18:00', + sendmail: false + }); + + const [requestType, setRequestType] = useState<'day' | 'time' | 'out'>('day'); // day: 휴가, time: 대체, out: 외출 + + useEffect(() => { + if (isOpen) { + loadCodes(); + if (userLevel >= 5) { + loadUsers(); + } + + if (initialData) { + setFormData({ ...initialData }); + // Determine request type based on data + if (initialData.cate === '외출') { + setRequestType('out'); + } else if (initialData.cate === '대체') { + setRequestType('time'); + } else { + setRequestType('day'); + } + } else { + // Reset form for new entry + setFormData({ + idx: 0, + gcode: '', + uid: currentUserId, + cate: '', + sdate: new Date().toISOString().split('T')[0], + edate: new Date().toISOString().split('T')[0], + Remark: '', + wuid: currentUserId, + wdate: '', + Response: '', + conf: 0, + HolyReason: '', + HolyBackup: '', + HolyLocation: '', + HolyDays: 0, + HolyTimes: 0, + stime: '09:00', + etime: '18:00', + sendmail: false + }); + setRequestType('day'); + } + } + }, [isOpen, initialData, currentUserId]); + + const loadCodes = async () => { + try { + const [cateRes, reasonRes, locationRes, backupRes] = await Promise.all([ + comms.getCommonList('50'), + comms.getCommonList('51'), + comms.getCommonList('52'), + comms.getCommonList('53') + ]); + setCodes({ + cate: cateRes || [], + reason: reasonRes || [], + location: locationRes || [], + backup: backupRes || [] + }); + + // Set default category if new + if (!initialData && cateRes && cateRes.length > 0) { + setFormData(prev => ({ ...prev, cate: cateRes[0].svalue })); + } + } catch (error) { + console.error('Failed to load codes:', error); + } + }; + + const loadUsers = async () => { + try { + const userList = await comms.getUserList(''); + if (userList && userList.length > 0) { + const mappedUsers = userList.map((u: any) => ({ + id: u.id || u.Id, + name: u.name || u.NameK || u.id + })); + setUsers(mappedUsers); + } + } catch (error) { + console.error('Failed to load users:', error); + } + }; + + const handleSave = async () => { + // Validation + if (!formData.cate && requestType === 'day') { + alert('구분을 선택하세요.'); + return; + } + if (formData.sdate > formData.edate) { + alert('종료일이 시작일보다 빠를 수 없습니다.'); + return; + } + + // Prepare data based on type + const dataToSave = { ...formData }; + if (requestType === 'out') { + dataToSave.cate = '외출'; + dataToSave.HolyDays = 0; + // Calculate times if needed, or rely on user input? + // WinForms doesn't seem to auto-calc times for 'out', just saves stime/etime. + } else if (requestType === 'time') { + dataToSave.cate = '대체'; + dataToSave.HolyDays = 0; + } else { + // Day + dataToSave.HolyTimes = 0; + dataToSave.stime = ''; + dataToSave.etime = ''; + + // Calculate days + const start = new Date(dataToSave.sdate); + const end = new Date(dataToSave.edate); + const diffTime = Math.abs(end.getTime() - start.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; + dataToSave.HolyDays = diffDays; + } + + setLoading(true); + try { + const response = await comms.saveHolidayRequest(dataToSave); + if (response.Success) { + onSave(); + onClose(); + } else { + alert('저장 실패: ' + response.Message); + } + } catch (error) { + console.error('Save error:', error); + alert('저장 중 오류가 발생했습니다.'); + } finally { + setLoading(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

+ {formData.idx === 0 ? '휴가/외출 신청' : '신청 내역 수정'} +

+ +
+ + {/* Body */} +
+ {/* Request Type */} +
+ + + +
+ + {/* User Selection (Admin only) */} + {userLevel >= 5 && ( +
+ + +
+ )} + + {/* Date & Time */} +
+
+ + setFormData({ ...formData, sdate: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + setFormData({ ...formData, edate: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + {(requestType === 'time' || requestType === 'out') && ( +
+
+ + setFormData({ ...formData, stime: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + setFormData({ ...formData, etime: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ )} + + {/* Category & Reason */} +
+
+ + {requestType === 'day' ? ( + + ) : ( + + )} +
+
+ + +
+
+ + {/* Location & Backup */} +
+
+ + +
+
+ + +
+
+ + {/* Days & Times (Manual Override) */} +
+
+ + setFormData({ ...formData, HolyDays: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + disabled={requestType !== 'day'} + /> +
+
+ + setFormData({ ...formData, HolyTimes: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + disabled={requestType === 'day'} + /> +
+
+ + {/* Remark */} +
+ +