feat(service): Console_SendMail을 Windows 서비스로 변환

- MailService.cs 추가: ServiceBase 상속받는 Windows 서비스 클래스
- Program.cs 수정: 서비스/콘솔 모드 지원, 설치/제거 기능 추가
- 프로젝트 설정: System.ServiceProcess 참조 추가
- 배치 파일 추가: 서비스 설치/제거/콘솔실행 스크립트

주요 기능:
- Windows 서비스로 백그라운드 실행
- 명령행 인수로 모드 선택 (-install, -uninstall, -console)
- EventLog를 통한 서비스 로깅
- 안전한 서비스 시작/중지 처리

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ChiKyun Kim
2025-09-11 09:08:40 +09:00
parent 777fcd5d89
commit 6bd4f84192
49 changed files with 46882 additions and 102 deletions

View File

@@ -0,0 +1,308 @@
// LoginApp.jsx - React Login Component for GroupWare
const { useState, useEffect, useRef } = React;
function LoginApp() {
const [formData, setFormData] = useState({
gcode: '',
userId: '',
password: '',
rememberMe: false
});
const [userGroups, setUserGroups] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState({ type: '', text: '', show: false });
const [isFormReady, setIsFormReady] = useState(false);
const gcodeRef = useRef(null);
const userIdRef = useRef(null);
const passwordRef = useRef(null);
// 메시지 표시 함수
const showMessage = (type, text) => {
setMessage({ type, text, show: true });
setTimeout(() => {
setMessage(prev => ({ ...prev, show: false }));
}, 3000);
};
// 폼 데이터 업데이트
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
// 사용자 그룹 목록 로드
const loadUserGroups = async () => {
try {
const response = await fetch('/DashBoard/GetUserGroups');
const data = await response.json();
// 유효한 그룹만 필터링
const validGroups = data.filter(group => group.gcode && group.name);
setUserGroups(validGroups);
// 이전 로그인 정보 로드
await loadPreviousLoginInfo();
} catch (error) {
console.error('그룹 목록 로드 중 오류 발생:', error);
showMessage('error', '부서 목록을 불러오는 중 오류가 발생했습니다.');
}
};
// 이전 로그인 정보 로드
const loadPreviousLoginInfo = async () => {
try {
const response = await fetch('/Home/GetPreviousLoginInfo');
const result = await response.json();
if (result.Success && result.Data) {
const { Gcode, UserId } = result.Data;
setFormData(prev => ({
...prev,
gcode: Gcode || '',
userId: UserId ? UserId.split(';')[0] : ''
}));
// 포커스 설정
setTimeout(() => {
if (Gcode && UserId) {
passwordRef.current?.focus();
} else {
gcodeRef.current?.focus();
}
}, 100);
}
setIsFormReady(true);
} catch (error) {
console.error('이전 로그인 정보 로드 중 오류 발생:', error);
setIsFormReady(true);
setTimeout(() => {
gcodeRef.current?.focus();
}, 100);
}
};
// 로그인 처리
const handleLogin = async (e) => {
e.preventDefault();
const { gcode, userId, password, rememberMe } = formData;
// 유효성 검사
if (!gcode || !userId || !password) {
showMessage('error', '그룹코드/사용자ID/비밀번호를 입력해주세요.');
return;
}
setIsLoading(true);
try {
const response = await fetch('/Home/Login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
Gcode: gcode,
UserId: userId,
Password: password,
RememberMe: rememberMe
})
});
const data = await response.json();
if (data.Success) {
// 로그인 성공
showMessage('success', data.Message);
// WebView2에 로그인 성공 메시지 전송
if (window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage('LOGIN_SUCCESS');
}
// 리다이렉트 URL이 있으면 이동
if (data.RedirectUrl) {
setTimeout(() => {
window.location.href = data.RedirectUrl;
}, 1000);
}
} else {
// 로그인 실패
showMessage('error', data.Message || '로그인에 실패했습니다.');
}
} catch (error) {
console.error('로그인 요청 중 오류 발생:', error);
showMessage('error', '서버 연결에 실패했습니다. 다시 시도해주세요.');
} finally {
setIsLoading(false);
}
};
// 컴포넌트 마운트 시 실행
useEffect(() => {
loadUserGroups();
}, []);
return (
<div className="gradient-bg min-h-screen flex items-center justify-center p-4">
<div className="w-full max-w-md">
{/* 로그인 카드 */}
<div className="glass-effect rounded-3xl p-8 card-hover animate-bounce-in">
{/* 로고 및 제목 */}
<div className="text-center mb-8 animate-fade-in">
<div className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<h1 className="text-2xl font-bold text-white mb-2">GroupWare</h1>
<p className="text-white/70 text-sm">로그인하여 시스템에 접속하세요</p>
</div>
{/* 로그인 폼 */}
<form onSubmit={handleLogin} className="space-y-6 animate-slide-up">
{/* Gcode 드롭다운 */}
<div className="relative">
<select
ref={gcodeRef}
name="gcode"
value={formData.gcode}
onChange={handleInputChange}
className="input-field w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white focus:outline-none focus:border-primary-400 input-focus appearance-none"
required
disabled={!isFormReady}
>
<option value="" className="text-gray-800">부서를 선택하세요</option>
{userGroups.map(group => (
<option key={group.gcode} value={group.gcode} className="text-gray-800">
{group.name}
</option>
))}
</select>
<div className="absolute right-3 top-3 pointer-events-none">
<svg className="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
{/* 사용자 ID 입력 */}
<div className="relative">
<input
ref={userIdRef}
type="text"
name="userId"
value={formData.userId}
onChange={handleInputChange}
className="input-field w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/60 focus:outline-none focus:border-primary-400 input-focus"
placeholder="사원번호"
required
disabled={!isFormReady}
/>
<div className="absolute right-3 top-3">
<svg className="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
</div>
{/* 비밀번호 입력 */}
<div className="relative">
<input
ref={passwordRef}
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
className="input-field w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/60 focus:outline-none focus:border-primary-400 input-focus"
placeholder="비밀번호"
required
disabled={!isFormReady}
/>
<div className="absolute right-3 top-3">
<svg className="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
</div>
{/* 로그인 버튼 */}
<button
type="submit"
disabled={isLoading || !isFormReady}
className="w-full bg-primary-500 hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold py-3 px-4 rounded-xl transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2 focus:ring-offset-transparent"
>
<span className="flex items-center justify-center">
{isLoading ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
로그인 ...
</>
) : (
<>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
로그인
</>
)}
</span>
</button>
</form>
{/* 추가 옵션 */}
<div className="mt-6 text-center">
<div className="flex items-center justify-center space-x-4 text-sm">
<label className="flex items-center text-white/70 hover:text-white cursor-pointer transition-colors">
<input
type="checkbox"
name="rememberMe"
checked={formData.rememberMe}
onChange={handleInputChange}
className="mr-2 w-4 h-4 text-primary-500 bg-white/10 border-white/20 rounded focus:ring-primary-400 focus:ring-2"
/>
로그인 정보 저장
</label>
<a href="#" className="text-primary-300 hover:text-primary-200 transition-colors">비밀번호 찾기</a>
</div>
</div>
</div>
{/* 푸터 */}
<div className="text-center mt-6 animate-fade-in">
<p className="text-white/50 text-sm">
© 2024 GroupWare System. All rights reserved.
</p>
</div>
{/* 메시지 표시 */}
{message.show && (
<div className={`fixed top-4 left-1/2 transform -translate-x-1/2 px-6 py-3 rounded-lg shadow-lg animate-slide-up ${
message.type === 'error' ? 'bg-red-500' : 'bg-green-500'
} text-white`}>
<div className="flex items-center">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{message.type === 'error' ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
)}
</svg>
<span>{message.text}</span>
</div>
</div>
)}
</div>
</div>
);
}