commit
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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, "접근이 금지되었습니다."),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -57,6 +57,10 @@ public class AgentUser implements UserPrincipal {
|
||||
this.userRole = userRole;
|
||||
}
|
||||
|
||||
public void changePassword(String newPassword) {
|
||||
this.userPassword = newPassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return Arrays.stream(AgentUserRole.values())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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, "사용자 인증에 실패 하였습니다."),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -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 => `
|
||||
<tr>
|
||||
<td>${job.jobGroup}</td>
|
||||
<td>${job.jobName}</td>
|
||||
<td>${formatDateTime(job.firedTime)}</td>
|
||||
<td>${job.state}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
document.getElementById('recentJobsTable').innerHTML = recentJobs
|
||||
.map(({ jobGroup, jobName, firedTime, state }) => `
|
||||
<tr>
|
||||
<td>${jobGroup}</td>
|
||||
<td>${jobName}</td>
|
||||
<td>${formatDateTime(firedTime)}</td>
|
||||
<td>${state}</td>
|
||||
</tr>
|
||||
`).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})`;
|
||||
};
|
||||
@@ -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) => {
|
||||
</tr>
|
||||
`).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 = `
|
||||
<div class="card">
|
||||
<ul class="list-group list-group-flush">
|
||||
${createDetailItem('그룹', jobDetail.group, 'bi-people')}
|
||||
${createDetailItem('잡 이름', jobDetail.name, 'bi-briefcase')}
|
||||
${createDetailItem('설명', jobDetail.description || '-', 'bi-card-text')}
|
||||
${createDetailItem('스케줄', `<input type="text" class="form-control form-control-sm" id="cronExpression" value="${jobDetail.cronExpression}">`, 'bi-calendar-event')}
|
||||
${createDetailItem('다음 실행', formatDateTime(jobDetail.nextFireTime), 'bi-clock')}
|
||||
${createDetailItem('이전 실행', formatDateTime(jobDetail.previousFireTime), 'bi-clock-history')}
|
||||
${createDetailItem('상태', `<span class="badge ${getStatusBadgeClass(jobDetail.status)}">${jobDetail.status}</span>`, 'bi-activity')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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) => `
|
||||
<div class="card">
|
||||
<ul class="list-group list-group-flush">
|
||||
${createDetailItem('그룹', jobDetail.group, 'bi-people')}
|
||||
${createDetailItem('잡 이름', jobDetail.name, 'bi-briefcase')}
|
||||
${createDetailItem('설명', jobDetail.description || '-', 'bi-card-text')}
|
||||
${createDetailItem('스케줄', `<input type="text" class="form-control form-control-sm" id="cronExpression" value="${jobDetail.cronExpression}">`, 'bi-calendar-event')}
|
||||
${createDetailItem('다음 실행', formatDateTime(jobDetail.nextFireTime), 'bi-clock')}
|
||||
${createDetailItem('이전 실행', formatDateTime(jobDetail.previousFireTime), 'bi-clock-history')}
|
||||
${createDetailItem('상태', `<span class="badge ${getStatusBadgeClass(jobDetail.status)}">${jobDetail.status}</span>`, 'bi-activity')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const createDetailItem = (label, value, iconClass) => `
|
||||
<li class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
@@ -95,9 +79,7 @@ const createDetailItem = (label, value, iconClass) => `
|
||||
<i class="bi ${iconClass} text-primary me-2"></i>
|
||||
<strong>${label}</strong>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
${value}
|
||||
</div>
|
||||
<div class="col-8">${value}</div>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
@@ -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 = `<span class="badge ${getStatusBadgeClass(newStatus)}">${newStatus}</span>`;
|
||||
break;
|
||||
}
|
||||
const targetRow = Array.from(tableRows).find(row =>
|
||||
row.cells[0].textContent === group && row.cells[1].textContent === name
|
||||
);
|
||||
if (targetRow) {
|
||||
targetRow.cells[3].innerHTML = `<span class="badge ${getStatusBadgeClass(newStatus)}">${newStatus}</span>`;
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
});
|
||||
const resetForm = ({ target }) => target.querySelector('form').reset();
|
||||
@@ -40,6 +40,11 @@
|
||||
<i class="bi bi-person-plus-fill me-2"></i>Sign up
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-end mt-3">
|
||||
<span id="changePassword" class="text-info text-decoration-none" style="cursor: pointer;">
|
||||
<small><i class="bi bi-key-fill me-1"></i>Reset your password</small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,6 +101,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 비밀번호 변경 모달 -->
|
||||
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-labelledby="changePasswordModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-info text-white">
|
||||
<h5 class="modal-title" id="changePasswordModalLabel">Reset Password</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="changePasswordForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-person"></i></span>
|
||||
<input type="text" class="form-control" id="changePasswordUserId" name="userId" placeholder="아이디" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-key-fill"></i></span>
|
||||
<input type="password" class="form-control" id="newPassword" name="newPassword" placeholder="새 비밀번호" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="bi bi-x-circle me-2"></i>Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-outline-info" id="changePasswordSubmit">
|
||||
<i class="bi bi-check-circle me-2"></i>Change
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" th:src="@{/js/pages/sign/sign-in.js}" defer></script>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user