- 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>
308 lines
14 KiB
JavaScript
308 lines
14 KiB
JavaScript
// 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>
|
|
);
|
|
} |