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:
308
Project/Web/wwwroot/react/LoginApp.jsx
Normal file
308
Project/Web/wwwroot/react/LoginApp.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user