This commit is contained in:
mindol1004
2024-10-08 18:12:37 +09:00
parent 698d7db4a2
commit bf50ab2e21
19 changed files with 342 additions and 276 deletions

View File

@@ -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) {

View File

@@ -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, "접근이 금지되었습니다."),

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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())

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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()));
}
}

View File

@@ -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)

View File

@@ -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");

View File

@@ -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) {

View File

@@ -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, "사용자 인증에 실패 하였습니다."),

View File

@@ -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());
}

View File

@@ -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;
}
};

View File

@@ -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})`;
};

View File

@@ -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);
}
};

View File

@@ -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();

View File

@@ -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>