diff --git a/batch-quartz/src/main/java/com/spring/common/error/BizBaseException.java b/batch-quartz/src/main/java/com/spring/common/error/BizBaseException.java index 5910407..ec5b3be 100644 --- a/batch-quartz/src/main/java/com/spring/common/error/BizBaseException.java +++ b/batch-quartz/src/main/java/com/spring/common/error/BizBaseException.java @@ -8,8 +8,8 @@ public class BizBaseException extends RuntimeException { private final ErrorRule errorRule; public BizBaseException() { - super(ExceptionRule.SYSTE_ERROR.getMessage()); - this.errorRule = ExceptionRule.SYSTE_ERROR; + super(ExceptionRule.SYSTEM_ERROR.getMessage()); + this.errorRule = ExceptionRule.SYSTEM_ERROR; } public BizBaseException(ErrorRule exceptionRule) { diff --git a/batch-quartz/src/main/java/com/spring/common/error/ExceptionRule.java b/batch-quartz/src/main/java/com/spring/common/error/ExceptionRule.java index 979657b..b158296 100644 --- a/batch-quartz/src/main/java/com/spring/common/error/ExceptionRule.java +++ b/batch-quartz/src/main/java/com/spring/common/error/ExceptionRule.java @@ -9,7 +9,7 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public enum ExceptionRule implements ErrorRule { - SYSTE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "시스템 오류 입니다."), + SYSTEM_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "시스템 오류 입니다."), BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."), FORBIDDEN(HttpStatus.FORBIDDEN, "접근이 금지되었습니다."), diff --git a/batch-quartz/src/main/java/com/spring/domain/user/api/PasswordChangeApi.java b/batch-quartz/src/main/java/com/spring/domain/user/api/PasswordChangeApi.java new file mode 100644 index 0000000..1b24197 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/api/PasswordChangeApi.java @@ -0,0 +1,27 @@ +package com.spring.domain.user.api; + +import javax.validation.Valid; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.spring.domain.user.dto.PasswordChangeRequest; +import com.spring.domain.user.service.PasswordChangeService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/user") +public class PasswordChangeApi { + + private final PasswordChangeService passwordChangeService; + + @PostMapping("/password-change") + public void passwordChange(@RequestBody @Valid PasswordChangeRequest request) { + passwordChangeService.changePassword(request); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/dto/PasswordChangeRequest.java b/batch-quartz/src/main/java/com/spring/domain/user/dto/PasswordChangeRequest.java new file mode 100644 index 0000000..566cbac --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/dto/PasswordChangeRequest.java @@ -0,0 +1,18 @@ +package com.spring.domain.user.dto; + +import javax.validation.constraints.NotBlank; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class PasswordChangeRequest { + + @NotBlank(message = "사용자ID는 필수값 입니다.") + private final String userId; + + @NotBlank(message = "비밀번호는 필수값 입니다.") + private final String newPassword; + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUser.java b/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUser.java index 4645f76..1dd68df 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUser.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUser.java @@ -57,6 +57,10 @@ public class AgentUser implements UserPrincipal { this.userRole = userRole; } + public void changePassword(String newPassword) { + this.userPassword = newPassword; + } + @Override public Collection getAuthorities() { return Arrays.stream(AgentUserRole.values()) diff --git a/batch-quartz/src/main/java/com/spring/domain/user/error/PasswordMismatchException.java b/batch-quartz/src/main/java/com/spring/domain/user/error/PasswordMismatchException.java new file mode 100644 index 0000000..7120881 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/error/PasswordMismatchException.java @@ -0,0 +1,11 @@ +package com.spring.domain.user.error; + +import com.spring.common.error.BizBaseException; + +public class PasswordMismatchException extends BizBaseException { + + public PasswordMismatchException() { + super(UserRule.CURRENT_PASSWORD_MISMATCH); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/error/PasswordSameException.java b/batch-quartz/src/main/java/com/spring/domain/user/error/PasswordSameException.java new file mode 100644 index 0000000..f9736ff --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/error/PasswordSameException.java @@ -0,0 +1,11 @@ +package com.spring.domain.user.error; + +import com.spring.common.error.BizBaseException; + +public class PasswordSameException extends BizBaseException { + + public PasswordSameException() { + super(UserRule.NEW_PASSWORD_SAME_AS_CURRENT); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/error/UserRule.java b/batch-quartz/src/main/java/com/spring/domain/user/error/UserRule.java index 964124f..4941a85 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/error/UserRule.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/error/UserRule.java @@ -13,7 +13,9 @@ public enum UserRule implements ErrorRule { USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다."), - USER_ID_CONFLICT(HttpStatus.CONFLICT, "중복된 아이디 입니다."); + USER_ID_CONFLICT(HttpStatus.CONFLICT, "중복된 아이디 입니다."), + CURRENT_PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "현재 비밀번호가 일치하지 않습니다."), + NEW_PASSWORD_SAME_AS_CURRENT(HttpStatus.BAD_REQUEST, "새 비밀번호는 현재 비밀번호와 달라야 합니다."); private final HttpStatus status; private String message; diff --git a/batch-quartz/src/main/java/com/spring/domain/user/service/PasswordChangeService.java b/batch-quartz/src/main/java/com/spring/domain/user/service/PasswordChangeService.java new file mode 100644 index 0000000..2be635e --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/service/PasswordChangeService.java @@ -0,0 +1,31 @@ +package com.spring.domain.user.service; + +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.spring.domain.user.dto.PasswordChangeRequest; +import com.spring.domain.user.entity.AgentUser; +import com.spring.domain.user.error.PasswordSameException; +import com.spring.domain.user.error.UserNotFoundException; +import com.spring.domain.user.repository.AgentUserRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class PasswordChangeService { + + private final AgentUserRepository agentUserRepository; + private final PasswordEncoder passwordEncoder; + + @Transactional + public void changePassword(PasswordChangeRequest request) { + AgentUser user = agentUserRepository.findByUserId(request.getUserId()).orElseThrow(UserNotFoundException::new); + if (passwordEncoder.matches(request.getNewPassword(), user.getPassword())) { + throw new PasswordSameException(); + } + user.changePassword(passwordEncoder.encode(request.getNewPassword())); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/service/UserPrincipalService.java b/batch-quartz/src/main/java/com/spring/domain/user/service/UserPrincipalService.java index 301b691..cf9f725 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/service/UserPrincipalService.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/service/UserPrincipalService.java @@ -8,9 +8,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.spring.domain.user.error.UserNotFoundException; -import com.spring.domain.user.error.UserRule; import com.spring.domain.user.repository.AgentUserRepository; import com.spring.infra.security.domain.UserPrincipal; +import com.spring.infra.security.error.SecurityAuthException; +import com.spring.infra.security.error.SecurityExceptionRule; import lombok.RequiredArgsConstructor; @@ -24,7 +25,7 @@ public class UserPrincipalService implements UserDetailsService { @Override public UserPrincipal loadUserByUsername(String username) throws UsernameNotFoundException { return agentUserRepository.findByUserId(username) - .orElseThrow(() -> new UsernameNotFoundException(UserRule.USER_UNAUTHORIZED.getMessage())); + .orElseThrow(() -> new SecurityAuthException(SecurityExceptionRule.USER_UNAUTHORIZED)); } @Transactional(readOnly = true) diff --git a/batch-quartz/src/main/java/com/spring/infra/security/config/PermittedURI.java b/batch-quartz/src/main/java/com/spring/infra/security/config/PermittedURI.java index 59e5223..7223d07 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/config/PermittedURI.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/config/PermittedURI.java @@ -12,6 +12,7 @@ public enum PermittedURI { FAVICON_URI("/favicon.ico"), USER_CONFLICT_URI("/api/user/conflict/{userId}"), USER_SIGN_UP("/api/user/sign-up"), + PASSWOD_CHANGE("/api/user/password-change"), USER_SIGN_IN("/sign-in"), USER_SIGN_OUT("/sign-out"); diff --git a/batch-quartz/src/main/java/com/spring/infra/security/error/SecurityAuthException.java b/batch-quartz/src/main/java/com/spring/infra/security/error/SecurityAuthException.java index 5cfe868..70f8c6e 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/error/SecurityAuthException.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/error/SecurityAuthException.java @@ -11,7 +11,7 @@ public class SecurityAuthException extends AuthenticationException { public SecurityAuthException(String msg) { super(msg); - this.exceptionRule = null; + this.exceptionRule = SecurityExceptionRule.SYSTEM_ERROR; } public SecurityAuthException(SecurityExceptionRule exceptionRule) { diff --git a/batch-quartz/src/main/java/com/spring/infra/security/error/SecurityExceptionRule.java b/batch-quartz/src/main/java/com/spring/infra/security/error/SecurityExceptionRule.java index 0300e39..2206c9a 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/error/SecurityExceptionRule.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/error/SecurityExceptionRule.java @@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public enum SecurityExceptionRule implements ErrorRule { + SYSTEM_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "시스템 오류 입니다."), UNSUPPORTED_MEDIA_ERROR(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "지원되지 않는 유형 입니다."), USER_BAD_REQUEST(HttpStatus.BAD_REQUEST, "사용자 정보가 올바르지 않습니다."), USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다."), diff --git a/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java b/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java index bacf88e..646d1fe 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java @@ -29,7 +29,7 @@ public class UserAuthenticationProvider implements AuthenticationProvider { String password = String.valueOf(authentication.getCredentials()); UserPrincipal user = (UserPrincipal) userDetailsService.loadUserByUsername(username); if (isNotMatches(password, user.getPassword())) { - throw new SecurityAuthException(SecurityExceptionRule.USER_NOT_PASSWORD.getMessage()); + throw new SecurityAuthException(SecurityExceptionRule.USER_NOT_PASSWORD); } return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()); } diff --git a/batch-quartz/src/main/resources/static/js/apis/sign-api.js b/batch-quartz/src/main/resources/static/js/apis/sign-api.js index 88ed342..227f2ad 100644 --- a/batch-quartz/src/main/resources/static/js/apis/sign-api.js +++ b/batch-quartz/src/main/resources/static/js/apis/sign-api.js @@ -22,6 +22,11 @@ const signService = { isConflictUserId: async (userId) => { const response = await apiClient.get(`/api/user/conflict/${userId}`); return response.data.data; + }, + + changePassword: async (userId, newPassword) => { + const response = await apiClient.post('/api/user/password-change', { userId, newPassword }); + return response.data.data; } }; diff --git a/batch-quartz/src/main/resources/static/js/pages/dashboard/dashboard.js b/batch-quartz/src/main/resources/static/js/pages/dashboard/dashboard.js index 7e7c28b..adb846a 100644 --- a/batch-quartz/src/main/resources/static/js/pages/dashboard/dashboard.js +++ b/batch-quartz/src/main/resources/static/js/pages/dashboard/dashboard.js @@ -2,6 +2,7 @@ import { formatDateTime } from '../../common/common.js'; import dashBoardService from '../../apis/dashboard-api.js'; let selectedMonth; + document.addEventListener('DOMContentLoaded', () => { initMonthPicker(); fetchDataAndRender(); @@ -9,8 +10,7 @@ document.addEventListener('DOMContentLoaded', () => { const initMonthPicker = () => { const monthPicker = document.getElementById('monthPicker'); - const currentDate = dayjs(); - selectedMonth = currentDate.format('YYYY-MM'); + selectedMonth = dayjs().format('YYYY-MM'); monthPicker.value = selectedMonth; monthPicker.addEventListener('click', (event) => { @@ -18,16 +18,18 @@ const initMonthPicker = () => { monthPicker.showPicker(); }); - monthPicker.addEventListener('change', (event) => { - selectedMonth = event.target.value; - if(selectedMonth) fetchDataAndRender(); + monthPicker.addEventListener('change', ({ target: { value } }) => { + selectedMonth = value; + if (selectedMonth) fetchDataAndRender(); }); }; const fetchDataAndRender = async () => { const [year, month] = selectedMonth.split('-'); - const batchData = await dashBoardService.getBatchJobExecutionData(year, month); - const recentJobs = await dashBoardService.getRecentJobs(); + const [batchData, recentJobs] = await Promise.all([ + dashBoardService.getBatchJobExecutionData(year, month), + dashBoardService.getRecentJobs() + ]); renderBatchExecutionTimeChart(batchData.jobAvgSummary); renderBatchStatusChart(batchData.statusCounts); @@ -40,37 +42,29 @@ const chartOptions = { responsive: true, maintainAspectRatio: false, plugins: { - legend: { - position: 'top', - }, - title: { - font: { - size: 16 - } - } + legend: { position: 'top' }, + title: { font: { size: 16 } } } }; let batchExecutionTimeChart; const renderBatchExecutionTimeChart = (data) => { if (batchExecutionTimeChart) batchExecutionTimeChart.destroy(); - const jobExecutionTimes = {}; - data.forEach(job => { - if (job.endTime && job.startTime) { - const duration = (new Date(job.endTime) - new Date(job.startTime)) / 1000; // 초 단위 - if (!jobExecutionTimes[job.jobName]) { - jobExecutionTimes[job.jobName] = { total: 0, count: 0 }; - } - jobExecutionTimes[job.jobName].total += duration; - jobExecutionTimes[job.jobName].count++; - } - }); - const averageExecutionTimes = Object.entries(jobExecutionTimes).reduce((acc, [jobName, job]) => { - acc[jobName] = job.total / job.count; + const jobExecutionTimes = data.reduce((acc, job) => { + if (job.endTime && job.startTime) { + const duration = (new Date(job.endTime) - new Date(job.startTime)) / 1000; + if (!acc[job.jobName]) acc[job.jobName] = { total: 0, count: 0 }; + acc[job.jobName].total += duration; + acc[job.jobName].count++; + } return acc; }, {}); + const averageExecutionTimes = Object.fromEntries( + Object.entries(jobExecutionTimes).map(([jobName, { total, count }]) => [jobName, total / count]) + ); + const ctx = document.getElementById('batchExecutionTimeChart').getContext('2d'); batchExecutionTimeChart = new Chart(ctx, { type: 'bar', @@ -90,21 +84,14 @@ const renderBatchExecutionTimeChart = (data) => { ...chartOptions.plugins, tooltip: { callbacks: { - label: (context) => { - const label = context.dataset.label || ''; - const value = context.parsed.y.toFixed(2); - return `${label}: ${value} 초`; - } + label: (context) => `${context.dataset.label || ''}: ${context.parsed.y.toFixed(2)} 초` } } }, scales: { y: { beginAtZero: true, - title: { - display: true, - text: '시간 (초)' - } + title: { display: true, text: '시간 (초)' } } } } @@ -114,16 +101,12 @@ const renderBatchExecutionTimeChart = (data) => { let batchStatusChart; const renderBatchStatusChart = (data) => { if (batchStatusChart) batchStatusChart.destroy(); - const statusTotals = data.reduce((acc, item) => { - if (!acc[item.status]) { - acc[item.status] = 0; - } - acc[item.status] += item.count; + + const statusTotals = data.reduce((acc, { status, count }) => { + acc[status] = (acc[status] || 0) + count; return acc; }, {}); - const labels = Object.keys(statusTotals); - const counts = Object.values(statusTotals); const statusColors = { 'COMPLETED': 'rgba(75, 192, 192, 0.6)', 'FAILED': 'rgba(255, 99, 132, 0.6)', @@ -138,9 +121,9 @@ const renderBatchStatusChart = (data) => { batchStatusChart = new Chart(ctx, { type: 'pie', data: { - labels: labels, + labels: Object.keys(statusTotals), datasets: [{ - data: counts, + data: Object.values(statusTotals), backgroundColor: Object.values(statusColors) }] }, @@ -151,18 +134,11 @@ const renderBatchStatusChart = (data) => { tooltip: { callbacks: { title: () => null, - label: (context) => { - const label = context.label || ''; - const value = context.parsed || 0; - return `${label}: ${value}`; - }, - afterLabel: (context) => { - const status = context.label; - return data - .filter(item => item.status === status) - .map(item => ` ${item.jobName}: ${item.count}`) - .join('\n'); - } + label: ({ label, parsed }) => `${label}: ${parsed}`, + afterLabel: ({ label }) => data + .filter(item => item.status === label) + .map(item => ` ${item.jobName}: ${item.count}`) + .join('\n') } } } @@ -173,53 +149,33 @@ const renderBatchStatusChart = (data) => { let hourlyJobExecutionChart; const renderHourlyJobExecutionChart = (data) => { if (hourlyJobExecutionChart) hourlyJobExecutionChart.destroy(); + const ctx = document.getElementById('hourlyJobExecutionChart').getContext('2d'); - const hours = Array.from({length: 24}, (_, i) => i); + const hours = Array.from({ length: 24 }, (_, i) => i); const jobNames = [...new Set(data.map(item => item.jobName))]; - const datasets = jobNames.map(jobName => { - const jobData = hours.map(hour => { - const hourData = data.find(item => item.jobName === jobName && item.hour === hour); - return hourData ? hourData.count : 0; - }); - - return { - label: jobName, - data: jobData, - borderColor: getRandomColor(), - backgroundColor: 'rgba(0, 0, 0, 0.1)', - fill: false - }; - }); + + const datasets = jobNames.map(jobName => ({ + label: jobName, + data: hours.map(hour => data.find(item => item.jobName === jobName && item.hour === hour)?.count || 0), + borderColor: getRandomColor(), + backgroundColor: 'rgba(0, 0, 0, 0.1)', + fill: false + })); hourlyJobExecutionChart = new Chart(ctx, { type: 'line', - data: { - labels: hours.map(hour => `${hour}:00`), - datasets: datasets - }, + data: { labels: hours.map(hour => `${hour}:00`), datasets }, options: { ...chartOptions, plugins: { ...chartOptions.plugins, - tooltip: { - mode: 'index', - intersect: false, - } + tooltip: { mode: 'index', intersect: false } }, scales: { - x: { - display: true, - title: { - display: true, - text: '시간' - } - }, + x: { display: true, title: { display: true, text: '시간' } }, y: { display: true, - title: { - display: true, - text: '실행 횟수' - }, + title: { display: true, text: '실행 횟수' }, suggestedMin: 0, beginAtZero: true } @@ -231,33 +187,26 @@ const renderHourlyJobExecutionChart = (data) => { let dailyJobExecutionsChart; const renderDailyJobExecutionsChart = (data) => { if (dailyJobExecutionsChart) dailyJobExecutionsChart.destroy(); + const ctx = document.getElementById('dailyJobExecutionsChart').getContext('2d'); - const [year, month] = selectedMonth.split('-'); const firstDay = new Date(year, month - 1, 1); const lastDay = new Date(year, month, 0); - const endDate = new Date(lastDay.getTime()); - - const dates = []; - for (let d = new Date(firstDay); d <= endDate; d = new Date(d.setDate(d.getDate() + 1))) { - dates.push(d.toISOString().split('T')[0]); - } + + const dates = Array.from({ length: lastDay.getDate() }, (_, i) => + new Date(year, month - 1, i + 1).toISOString().split('T')[0] + ); const groupedData = data.reduce((acc, item) => { const date = item.executionDate.split('T')[0]; - if (!acc[date]) { - acc[date] = {}; - } - if (!acc[date][item.jobName]) { - acc[date][item.jobName] = 0; - } - acc[date][item.jobName] += item.executionCount; + if (!acc[date]) acc[date] = {}; + acc[date][item.jobName] = (acc[date][item.jobName] || 0) + item.executionCount; return acc; }, {}); const jobNames = [...new Set(data.map(item => item.jobName))]; - const datasets = jobNames.map((jobName) => { + const datasets = jobNames.map(jobName => { const color = getRandomColor(); return { label: jobName, @@ -274,43 +223,28 @@ const renderDailyJobExecutionsChart = (data) => { dailyJobExecutionsChart = new Chart(ctx, { type: 'line', - data: { - datasets: datasets - }, + data: { datasets }, options: { ...chartOptions, plugins: { ...chartOptions.plugins, tooltip: { callbacks: { - title: (context) => { - return luxon.DateTime.fromJSDate(context[0].parsed.x).toFormat('yyyy-MM-dd'); - } + title: (context) => luxon.DateTime.fromJSDate(context[0].parsed.x).toFormat('yyyy-MM-dd') } } }, scales: { x: { type: 'time', - time: { - unit: 'day', - displayFormats: { - day: 'MM-dd' - } - }, - title: { - display: true, - text: '날짜' - }, + time: { unit: 'day', displayFormats: { day: 'MM-dd' } }, + title: { display: true, text: '날짜' }, min: firstDay, max: lastDay }, y: { beginAtZero: true, - title: { - display: true, - text: '실행 횟수' - } + title: { display: true, text: '실행 횟수' } } } } @@ -318,20 +252,18 @@ const renderDailyJobExecutionsChart = (data) => { }; const renderRecentJobsTable = (recentJobs) => { - const tableBody = document.getElementById('recentJobsTable'); - tableBody.innerHTML = recentJobs.map(job => ` - - ${job.jobGroup} - ${job.jobName} - ${formatDateTime(job.firedTime)} - ${job.state} - - `).join(''); + document.getElementById('recentJobsTable').innerHTML = recentJobs + .map(({ jobGroup, jobName, firedTime, state }) => ` + + ${jobGroup} + ${jobName} + ${formatDateTime(firedTime)} + ${state} + + `).join(''); }; const getRandomColor = () => { - const r = Math.floor(Math.random() * 255); - const g = Math.floor(Math.random() * 255); - const b = Math.floor(Math.random() * 255); + const [r, g, b] = Array.from({ length: 3 }, () => Math.floor(Math.random() * 255)); return `rgb(${r}, ${g}, ${b})`; }; \ No newline at end of file diff --git a/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js b/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js index d726231..8a3da92 100644 --- a/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js +++ b/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js @@ -3,30 +3,26 @@ import scheduleService from '../../apis/schedule-api.js'; document.addEventListener('DOMContentLoaded', () => { fetchDataAndRender(); - - const searchForm = document.getElementById('searchForm'); - searchForm.addEventListener('submit', async (e) => { - e.preventDefault(); - fetchDataAndRender(); - }); - - const refreshJobBtn = document.getElementById('refreshJobBtn'); - refreshJobBtn.addEventListener('click', refreshJobs); - + setupEventListeners(); manageTooltips.init(); }); +const setupEventListeners = () => { + document.getElementById('searchForm').addEventListener('submit', async (e) => { + e.preventDefault(); + fetchDataAndRender(); + }); + document.getElementById('refreshJobBtn').addEventListener('click', refreshJobs); +}; + const fetchDataAndRender = async () => { - const searchForm = document.getElementById('searchForm'); - const formData = new FormData(searchForm); - const searchParams = new URLSearchParams(formData); - const response = await scheduleService.getAllJobs(searchParams); - updateTable(response); + const searchParams = new URLSearchParams(new FormData(document.getElementById('searchForm'))); + const jobs = await scheduleService.getAllJobs(searchParams); + updateTable(jobs); }; const refreshJobs = async () => { - const result = await scheduleService.refreshJob(); - if (result) { + if (await scheduleService.refreshJob()) { alert('스케줄이 재적용 되었습니다.'); manageTooltips.hideAll(); fetchDataAndRender(); @@ -49,45 +45,33 @@ const updateTable = (jobs) => { `).join(''); - document.querySelectorAll('.detail-btn').forEach(btn => { - btn.addEventListener('click', showJobDetail); - }); + document.querySelectorAll('.detail-btn').forEach(btn => btn.addEventListener('click', showJobDetail)); }; -const showJobDetail = async (e) => { - const { group, name } = e.target.closest('button').dataset; +const showJobDetail = async ({ target }) => { + const { group, name } = target.closest('button').dataset; const jobDetail = await scheduleService.getJobDetail(group, name); - const detailContent = document.getElementById('scheduleDetailContent'); - detailContent.innerHTML = ` -
- -
- `; - - const modal = new bootstrap.Modal(document.getElementById('scheduleDetailModal')); - modal.show(); + document.getElementById('scheduleDetailContent').innerHTML = createDetailContent(jobDetail); + new bootstrap.Modal(document.getElementById('scheduleDetailModal')).show(); updateJobControlButtons(jobDetail.status); - - document.getElementById('pauseJobBtn').onclick = () => { - scheduleService.pauseJob(group, name).then(() => updateJobStatus(group, name, 'PAUSED')); - }; - document.getElementById('resumeJobBtn').onclick = () => { - scheduleService.resumeJob(group, name).then(() => updateJobStatus(group, name, 'NORMAL')); - }; - document.getElementById('updateCronBtn').onclick = () => { - updateCronExpression(group, name); - }; + setupDetailModalEventListeners(group, name); }; +const createDetailContent = (jobDetail) => ` +
+ +
+`; + const createDetailItem = (label, value, iconClass) => `
  • @@ -95,9 +79,7 @@ const createDetailItem = (label, value, iconClass) => ` ${label}
    -
    - ${value} -
    +
    ${value}
  • `; @@ -114,11 +96,15 @@ const getStatusBadgeClass = (status) => { return statusClasses[status] || 'bg-secondary'; }; +const setupDetailModalEventListeners = (group, name) => { + document.getElementById('pauseJobBtn').onclick = () => updateJobStatus(group, name, 'PAUSED', scheduleService.pauseJob); + document.getElementById('resumeJobBtn').onclick = () => updateJobStatus(group, name, 'NORMAL', scheduleService.resumeJob); + document.getElementById('updateCronBtn').onclick = () => updateCronExpression(group, name); +}; + const updateCronExpression = async (group, name) => { - const cronExpressionInput = document.getElementById('cronExpression'); - const newCronExpression = cronExpressionInput.value; - const result = await scheduleService.rescheduleJob(group, name, newCronExpression); - if (result) { + const newCronExpression = document.getElementById('cronExpression').value; + if (await scheduleService.rescheduleJob(group, name, newCronExpression)) { alert('스케쥴이 수정 되었습니다.'); fetchDataAndRender(); } else { @@ -127,69 +113,48 @@ const updateCronExpression = async (group, name) => { }; const updateJobControlButtons = (status) => { - const pauseJobBtn = document.getElementById('pauseJobBtn'); - const resumeJobBtn = document.getElementById('resumeJobBtn'); - const updateButtonState = (button, isEnabled) => { button.disabled = !isEnabled; button.classList.toggle(button.dataset.enabledClass, isEnabled); button.classList.toggle('btn-outline-secondary', !isEnabled); }; - const updateBothButtons = (pauseEnabled, resumeEnabled) => { - updateButtonState(pauseJobBtn, pauseEnabled); - updateButtonState(resumeJobBtn, resumeEnabled); - }; + const [pauseJobBtn, resumeJobBtn] = ['pauseJobBtn', 'resumeJobBtn'].map(id => document.getElementById(id)); + const isPaused = status === 'PAUSED'; + const isActive = !['COMPLETE', 'ERROR'].includes(status); - switch (status) { - case 'PAUSED': - updateBothButtons(false, true); - break; - case 'COMPLETE': - case 'ERROR': - updateBothButtons(false, false); - break; - case 'NORMAL': - case 'BLOCKED': - case 'NONE': - default: - updateBothButtons(true, false); - break; - } + updateButtonState(pauseJobBtn, isActive && !isPaused); + updateButtonState(resumeJobBtn, isPaused); }; -const updateJobStatus = async (group, name, newStatus) => { - const jobDetail = await scheduleService.getJobDetail(group, name); - jobDetail.status = newStatus; - - const statusElement = document.querySelector('#scheduleDetailContent .badge'); - statusElement.className = `badge ${getStatusBadgeClass(newStatus)}`; - statusElement.textContent = newStatus; +const updateJobStatus = async (group, name, newStatus, action) => { + if (await action(group, name)) { + const statusElement = document.querySelector('#scheduleDetailContent .badge'); + statusElement.className = `badge ${getStatusBadgeClass(newStatus)}`; + statusElement.textContent = newStatus; - updateJobControlButtons(newStatus); - updateTableJobStatus(group, name, newStatus); + updateJobControlButtons(newStatus); + updateTableJobStatus(group, name, newStatus); + } }; const updateTableJobStatus = (group, name, newStatus) => { const tableRows = document.querySelectorAll('tbody tr'); - for (let row of tableRows) { - const rowGroup = row.cells[0].textContent; - const rowName = row.cells[1].textContent; - if (rowGroup === group && rowName === name) { - const statusCell = row.cells[3]; - statusCell.innerHTML = `${newStatus}`; - break; - } + const targetRow = Array.from(tableRows).find(row => + row.cells[0].textContent === group && row.cells[1].textContent === name + ); + if (targetRow) { + targetRow.cells[3].innerHTML = `${newStatus}`; } }; const manageTooltips = { init: () => { - const tooltipElements = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); - tooltipElements.map(el => new bootstrap.Tooltip(el)); + document.querySelectorAll('[data-bs-toggle="tooltip"]') + .forEach(el => new bootstrap.Tooltip(el)); }, hideAll: () => { const tooltip = bootstrap.Tooltip.getInstance('#refreshJobBtn'); - setTimeout(() => { tooltip.hide(); }, 100); + setTimeout(() => tooltip.hide(), 100); } }; \ No newline at end of file diff --git a/batch-quartz/src/main/resources/static/js/pages/sign/sign-in.js b/batch-quartz/src/main/resources/static/js/pages/sign/sign-in.js index ccdf58c..173ee2b 100644 --- a/batch-quartz/src/main/resources/static/js/pages/sign/sign-in.js +++ b/batch-quartz/src/main/resources/static/js/pages/sign/sign-in.js @@ -1,40 +1,56 @@ import signService from '../../apis/sign-api.js'; document.addEventListener('DOMContentLoaded', () => { - const signupModal = new bootstrap.Modal(document.getElementById('signupModal')); - const signInButton = document.getElementById('signIn'); - const signupButton = document.getElementById('signUp'); - const signupForm = document.getElementById('signupForm'); + initializeElements(); + setupEventListeners(); +}); - signInButton.addEventListener('click', () => { - const username = document.getElementById('username').value; - const password = document.getElementById('password').value; - signService.signIn(username, password).then(response => { - if (response.status) { - window.location.href = response.redirectUrl; - } - }); +const initializeElements = () => { + window.signupModal = new bootstrap.Modal(document.getElementById('signupModal')); + window.changePasswordModal = new bootstrap.Modal(document.getElementById('changePasswordModal')); +}; + +const setupEventListeners = () => { + const eventMap = { + 'signIn': { event: 'click', handler: handleSignIn }, + 'signUp': { event: 'click', handler: () => signupModal.show() }, + 'signupForm': { event: 'submit', handler: handleSignUp }, + 'signupModal': { event: 'hidden.bs.modal', handler: resetForm }, + 'changePassword': { event: 'click', handler: () => changePasswordModal.show() }, + 'changePasswordForm': { event: 'submit', handler: handleChangePassword }, + 'changePasswordModal': { event: 'hidden.bs.modal', handler: resetForm } + }; + + Object.entries(eventMap).forEach(([id, { event, handler }]) => { + document.getElementById(id)?.addEventListener(event, handler); }); +}; - signupButton.addEventListener('click', () => { - signupModal.show(); - }); +const handleSignIn = async () => { + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + const { status, redirectUrl } = await signService.signIn(username, password); + if (status) window.location.href = redirectUrl; +}; - signupForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const userId = document.getElementById('userId').value; - const isConflict = await signService.isConflictUserId(userId); - if (!isConflict) { - const params = Object.fromEntries(new FormData(signupForm)); - signService.signUp(params).then(() => { - alert(`회원가입이 완료 되었습니다.`); - signupModal.hide(); - }); - } - }); +const handleSignUp = async (e) => { + e.preventDefault(); + const userId = document.getElementById('userId').value; + const isConflict = await signService.isConflictUserId(userId); + if (!isConflict) { + const params = Object.fromEntries(new FormData(e.target)); + await signService.signUp(params); + alert('회원가입이 완료되었습니다.'); + signupModal.hide(); + } +}; - document.getElementById('signupModal').addEventListener('hidden.bs.modal', () => { - signupForm.reset(); - }); +const handleChangePassword = async (e) => { + e.preventDefault(); + const [userId, newPassword] = ['changePasswordUserId', 'newPassword'].map(id => document.getElementById(id).value); + await signService.changePassword(userId, newPassword); + alert('비밀번호가 변경되었습니다.'); + changePasswordModal.hide(); +}; -}); \ No newline at end of file +const resetForm = ({ target }) => target.querySelector('form').reset(); \ No newline at end of file diff --git a/batch-quartz/src/main/resources/templates/pages/sign/sign-in.html b/batch-quartz/src/main/resources/templates/pages/sign/sign-in.html index 9b8d3bd..608b1d4 100644 --- a/batch-quartz/src/main/resources/templates/pages/sign/sign-in.html +++ b/batch-quartz/src/main/resources/templates/pages/sign/sign-in.html @@ -40,6 +40,11 @@ Sign up +
    + + Reset your password + +
    @@ -96,6 +101,42 @@ + + +