응답 관리 그룹 추가

This commit is contained in:
현성필
2025-02-05 16:08:00 +09:00
parent 58b7c43be5
commit 284ae15a67
16 changed files with 694 additions and 3 deletions
@@ -0,0 +1,129 @@
package com.eactive.testmaster.api.controller;
import com.eactive.testmaster.api.dto.MockRouteGroupSearch;
import com.eactive.testmaster.api.entity.MockRouteGroup;
import com.eactive.testmaster.api.repository.MockRouteGroupRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import javax.validation.Valid;
import java.util.List;
@Controller
@RequestMapping("/mgmt/routes/group")
public class MockRouteGroupController {
private static final String GROUP = "group";
private static final String GROUP_LIST_VIEW = "page/routes/groupList";
private static final String GROUP_EDIT = "page/routes/groupEdit";
private static final String REDIRECT_TO_LIST = "redirect:/mgmt/routes/group/list_view.do";
private final MockRouteGroupRepository mockRouteGroupRepository;
private final MockRouteGroupValidator mockRouteGroupValidator;
public MockRouteGroupController(MockRouteGroupRepository mockRouteGroupRepository,
MockRouteGroupValidator mockRouteGroupValidator) {
this.mockRouteGroupRepository = mockRouteGroupRepository;
this.mockRouteGroupValidator = mockRouteGroupValidator;
}
@InitBinder("group")
public void initBinder(WebDataBinder binder) {
binder.setValidator(mockRouteGroupValidator);
}
@GetMapping("/list_view.do")
@Secured("ROLE_MANAGE_ROUTE")
public ModelAndView listView(@ModelAttribute("groupSearch") MockRouteGroupSearch groupSearch,
Pageable pageable) {
ModelAndView mav = new ModelAndView(GROUP_LIST_VIEW);
Page<MockRouteGroup> page = mockRouteGroupRepository.findAll(
groupSearch.buildSpecification(), pageable);
mav.addObject("page", page);
mav.addObject("pageable", pageable);
mav.addObject("modelSearch", groupSearch);
return mav;
}
@GetMapping("/create_view.do")
@Secured("ROLE_MANAGE_ROUTE")
public ModelAndView createView() {
ModelAndView mav = new ModelAndView(GROUP_EDIT);
mav.addObject(GROUP, new MockRouteGroup());
return mav;
}
@GetMapping("/update_view.do")
@Secured("ROLE_MANAGE_ROUTE")
public ModelAndView updateView(@RequestParam("id") String id) {
ModelAndView mav = new ModelAndView(GROUP_EDIT);
MockRouteGroup group = mockRouteGroupRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Group not found"));
mav.addObject(GROUP, group);
return mav;
}
@PostMapping("/register.do")
@Secured("ROLE_MANAGE_ROUTE")
public String register(@Valid @ModelAttribute(GROUP) MockRouteGroup group,
BindingResult bindingResult,
ModelMap model) {
if (bindingResult.hasErrors()) {
model.addAttribute(GROUP, group);
return GROUP_EDIT;
}
try {
mockRouteGroupRepository.save(group);
return REDIRECT_TO_LIST;
} catch (Exception e) {
bindingResult.rejectValue("id", "error.save.failed",
"Failed to save group: " + e.getMessage());
model.addAttribute(GROUP, group);
return GROUP_EDIT;
}
}
@PostMapping("/update.do")
@Secured("ROLE_MANAGE_ROUTE")
public String update(@RequestParam("id") String id,
@Valid @ModelAttribute(GROUP) MockRouteGroup group,
BindingResult bindingResult,
ModelMap model) {
if (bindingResult.hasErrors()) {
model.addAttribute(GROUP, group);
return GROUP_EDIT;
}
try {
MockRouteGroup existingGroup = mockRouteGroupRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Group not found"));
existingGroup.setName(group.getName());
mockRouteGroupRepository.save(existingGroup);
return REDIRECT_TO_LIST;
} catch (Exception e) {
bindingResult.rejectValue("name", "error.update.failed",
"Failed to update group: " + e.getMessage());
model.addAttribute(GROUP, group);
return GROUP_EDIT;
}
}
@PostMapping("/delete.do")
@Secured("ROLE_MANAGE_ROUTE")
public String delete(@RequestParam("checkedIdForDel") List<String> ids) {
try {
mockRouteGroupRepository.deleteAllById(ids);
} catch (Exception e) {
// Log error but continue
}
return REDIRECT_TO_LIST;
}
}
@@ -0,0 +1,119 @@
package com.eactive.testmaster.api.controller;
import com.eactive.testmaster.api.dto.MockRouteGroupSearch;
import com.eactive.testmaster.api.entity.MockRouteGroup;
import com.eactive.testmaster.api.repository.MockRouteGroupRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/mock-route-groups")
public class MockRouteGroupRestController {
private final MockRouteGroupRepository mockRouteGroupRepository;
private final MockRouteGroupValidator mockRouteGroupValidator;
public MockRouteGroupRestController(MockRouteGroupRepository mockRouteGroupRepository,
MockRouteGroupValidator mockRouteGroupValidator) {
this.mockRouteGroupRepository = mockRouteGroupRepository;
this.mockRouteGroupValidator = mockRouteGroupValidator;
}
@GetMapping
public ResponseEntity<Page<MockRouteGroup>> listGroups(
@RequestParam(required = false) String name,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
MockRouteGroupSearch search = new MockRouteGroupSearch();
search.setName(name);
Pageable pageable = PageRequest.of(page, size);
Page<MockRouteGroup> groups = mockRouteGroupRepository.findAll(
search.buildSpecification(),
pageable
);
return ResponseEntity.ok(groups);
}
@GetMapping("/{id}")
public ResponseEntity<MockRouteGroup> getGroup(@PathVariable String id) {
return mockRouteGroupRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<?> createGroup(@RequestBody MockRouteGroup group) {
// Validate using the custom validator
Errors errors = new BeanPropertyBindingResult(group, "group");
mockRouteGroupValidator.validate(group, errors);
if (errors.hasErrors()) {
Map<String, String> validationErrors = new HashMap<>();
errors.getFieldErrors().forEach(error ->
validationErrors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(validationErrors);
}
try {
MockRouteGroup saved = mockRouteGroupRepository.save(group);
return ResponseEntity.ok(saved);
} catch (Exception e) {
return ResponseEntity.badRequest()
.body("Failed to create group: " + e.getMessage());
}
}
@PostMapping("/update/{id}")
public ResponseEntity<?> updateGroup(
@PathVariable String id, @RequestBody MockRouteGroup group) {
// Validate using the custom validator
Errors errors = new BeanPropertyBindingResult(group, "group");
mockRouteGroupValidator.validate(group, errors);
if (errors.hasErrors()) {
Map<String, String> validationErrors = new HashMap<>();
errors.getFieldErrors().forEach(error ->
validationErrors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(validationErrors);
}
try {
return mockRouteGroupRepository.findById(id)
.map(existingGroup -> {
existingGroup.setName(group.getName());
MockRouteGroup updated = mockRouteGroupRepository.save(existingGroup);
return ResponseEntity.ok(updated);
})
.orElse(ResponseEntity.notFound().build());
} catch (Exception e) {
return ResponseEntity.badRequest()
.body("Failed to update group: " + e.getMessage());
}
}
@PostMapping("/delete")
public ResponseEntity<Void> deleteGroups(@RequestBody List<String> ids) {
try {
mockRouteGroupRepository.deleteAllById(ids);
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
}
@@ -0,0 +1,49 @@
package com.eactive.testmaster.api.controller;
import com.eactive.testmaster.api.entity.MockRouteGroup;
import com.eactive.testmaster.api.repository.MockRouteGroupRepository;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
@Component
public class MockRouteGroupValidator implements Validator {
private final MockRouteGroupRepository mockRouteGroupRepository;
public MockRouteGroupValidator(MockRouteGroupRepository mockRouteGroupRepository) {
this.mockRouteGroupRepository = mockRouteGroupRepository;
}
@Override
public boolean supports(Class<?> clazz) {
return MockRouteGroup.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
MockRouteGroup group = (MockRouteGroup) target;
// Validate ID
if (group.getId() == null || group.getId().trim().isEmpty()) {
errors.rejectValue("id", "error.id.required", "Group ID is required");
} else if (!group.getId().matches("^[a-zA-Z0-9_-]+$")) {
errors.rejectValue("id", "error.id.invalid", "Group ID must contain only letters, numbers, underscores, and hyphens");
} else if (group.getId().length() > 50) {
errors.rejectValue("id", "error.id.length", "Group ID must not exceed 50 characters");
}
// Validate Name
if (group.getName() == null || group.getName().trim().isEmpty()) {
errors.rejectValue("name", "error.name.required", "Group name is required");
} else if (group.getName().length() > 100) {
errors.rejectValue("name", "error.name.length", "Group name must not exceed 100 characters");
}
// Check for duplicate ID on creation
if (group.getId() != null && !errors.hasFieldErrors("id")) {
mockRouteGroupRepository.findById(group.getId())
.ifPresent(existingGroup -> errors.rejectValue("id", "error.id.duplicate", "Group ID already exists"));
}
}
}
@@ -6,6 +6,7 @@ import com.eactive.testmaster.api.dto.MockRouteSearch;
import com.eactive.testmaster.api.dto.MockRouteUpdateDTO; import com.eactive.testmaster.api.dto.MockRouteUpdateDTO;
import com.eactive.testmaster.api.entity.MockRoute; import com.eactive.testmaster.api.entity.MockRoute;
import com.eactive.testmaster.api.mapper.MockRouteMapper; import com.eactive.testmaster.api.mapper.MockRouteMapper;
import com.eactive.testmaster.api.repository.MockRouteGroupRepository;
import com.eactive.testmaster.api.service.MockRouteService; import com.eactive.testmaster.api.service.MockRouteService;
import java.util.List; import java.util.List;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@@ -43,11 +44,14 @@ public class MockRouteMgmtController {
private final MockApiHandlerMapping mockApiHandlerMapping; private final MockApiHandlerMapping mockApiHandlerMapping;
public MockRouteMgmtController(MockRouteService mockRouteService, Validator validator, MockRouteMapper mockRouteMapper, MockApiHandlerMapping mockApiHandlerMapping) { private final MockRouteGroupRepository mockRouteGroupRepository;
public MockRouteMgmtController(MockRouteService mockRouteService, Validator validator, MockRouteMapper mockRouteMapper, MockApiHandlerMapping mockApiHandlerMapping, MockRouteGroupRepository mockRouteGroupRepository) {
this.mockRouteService = mockRouteService; this.mockRouteService = mockRouteService;
this.validator = validator; this.validator = validator;
this.mockRouteMapper = mockRouteMapper; this.mockRouteMapper = mockRouteMapper;
this.mockApiHandlerMapping = mockApiHandlerMapping; this.mockApiHandlerMapping = mockApiHandlerMapping;
this.mockRouteGroupRepository = mockRouteGroupRepository;
} }
public static final String MOCK_ROUTE_EDIT = "page/routes/mockRouteEdit"; public static final String MOCK_ROUTE_EDIT = "page/routes/mockRouteEdit";
@@ -61,6 +65,7 @@ public class MockRouteMgmtController {
mav.addObject("page", page); mav.addObject("page", page);
mav.addObject("pageable", pageable); mav.addObject("pageable", pageable);
mav.addObject("modelSearch", mockRouteSearch); mav.addObject("modelSearch", mockRouteSearch);
mav.addObject("groupList", mockRouteGroupRepository.findAll());
return mav; return mav;
} }
@@ -74,6 +79,7 @@ public class MockRouteMgmtController {
defaultResponse.setDefaultResponse("true"); defaultResponse.setDefaultResponse("true");
defaultRoute.getResponses().add(defaultResponse); defaultRoute.getResponses().add(defaultResponse);
mav.addObject(MOCK_ROUTE, defaultRoute); mav.addObject(MOCK_ROUTE, defaultRoute);
mav.addObject("groupList", mockRouteGroupRepository.findAll());
return mav; return mav;
} }
@@ -84,7 +90,7 @@ public class MockRouteMgmtController {
ModelAndView mav = new ModelAndView(MOCK_ROUTE_EDIT); ModelAndView mav = new ModelAndView(MOCK_ROUTE_EDIT);
MockRoute found = mockRouteService.findById(id); MockRoute found = mockRouteService.findById(id);
mav.addObject(MOCK_ROUTE, mockRouteMapper.map(found)); mav.addObject(MOCK_ROUTE, mockRouteMapper.map(found));
mav.addObject("groupList", mockRouteGroupRepository.findAll());
return mav; return mav;
} }
@@ -97,6 +103,7 @@ public class MockRouteMgmtController {
MockRouteRegisterDTO dto = mockRouteMapper.map(found); MockRouteRegisterDTO dto = mockRouteMapper.map(found);
dto.setId(null); dto.setId(null);
mav.addObject(MOCK_ROUTE, dto); mav.addObject(MOCK_ROUTE, dto);
mav.addObject("groupList", mockRouteGroupRepository.findAll());
return mav; return mav;
} }
@@ -0,0 +1,41 @@
package com.eactive.testmaster.api.dto;
import com.eactive.testmaster.common.search.BaseSearch;
import com.eactive.testmaster.common.search.ColumnSearchModel;
import com.eactive.testmaster.common.search.SearchCondition;
import com.eactive.testmaster.common.search.SearchModel;
import com.eactive.testmaster.api.entity.MockRouteGroup;
import java.util.ArrayList;
import java.util.List;
public class MockRouteGroupSearch implements BaseSearch<MockRouteGroup> {
private String id;
private String name;
@Override
public List<SearchModel> buildSearchCondition() {
List<SearchModel> models = new ArrayList<>();
models.add(new ColumnSearchModel("id", SearchCondition.LIKE, id));
models.add(new ColumnSearchModel("name", SearchCondition.LIKE, name));
return models;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -20,6 +20,8 @@ public class MockRouteRegisterDTO {
@NotEmpty @NotEmpty
private String name; private String name;
private String group;
private String description; private String description;
private String requestScript; private String requestScript;
@@ -82,4 +84,12 @@ public class MockRouteRegisterDTO {
public void setRequestScript(String requestScript) { public void setRequestScript(String requestScript) {
this.requestScript = requestScript; this.requestScript = requestScript;
} }
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
} }
@@ -12,10 +12,13 @@ public class MockRouteSearch implements BaseSearch<MockRoute> {
private String name; private String name;
private String group;
@Override @Override
public List<SearchModel> buildSearchCondition() { public List<SearchModel> buildSearchCondition() {
List<SearchModel> models = new ArrayList<>(); List<SearchModel> models = new ArrayList<>();
models.add(new ColumnSearchModel("name", SearchCondition.LIKE, name)); models.add(new ColumnSearchModel("name", SearchCondition.LIKE, name));
models.add(new ColumnSearchModel("group", SearchCondition.EQUAL, group));
return models; return models;
} }
@@ -27,4 +30,11 @@ public class MockRouteSearch implements BaseSearch<MockRoute> {
this.name = name; this.name = name;
} }
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
} }
@@ -22,6 +22,7 @@ public class MockRouteUpdateDTO {
private String requestScript; private String requestScript;
private String group;
@Size(min = 1, message = "Response must have at least one item") @Size(min = 1, message = "Response must have at least one item")
private List<MockResponseDTO> responses = new ArrayList<>(); private List<MockResponseDTO> responses = new ArrayList<>();
@@ -81,4 +82,12 @@ public class MockRouteUpdateDTO {
public void setRequestScript(String requestScript) { public void setRequestScript(String requestScript) {
this.requestScript = requestScript; this.requestScript = requestScript;
} }
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
} }
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonManagedReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.persistence.CascadeType; import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType; import javax.persistence.FetchType;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
@@ -30,6 +31,9 @@ public class MockRoute {
private String description; private String description;
@Column(name = "MOCK_GROUP")
private String group;
@Lob @Lob
private String requestScript; private String requestScript;
@@ -85,7 +89,6 @@ public class MockRoute {
this.description = description; this.description = description;
} }
public String getRequestScript() { public String getRequestScript() {
return requestScript; return requestScript;
} }
@@ -93,4 +96,12 @@ public class MockRoute {
public void setRequestScript(String requestScript) { public void setRequestScript(String requestScript) {
this.requestScript = requestScript; this.requestScript = requestScript;
} }
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
} }
@@ -0,0 +1,32 @@
package com.eactive.testmaster.api.entity;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "MOCK_ROUTE_GROUP")
public class MockRouteGroup {
@Id
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,9 @@
package com.eactive.testmaster.api.repository;
import com.eactive.testmaster.api.entity.MockRouteGroup;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface MockRouteGroupRepository extends JpaRepository<MockRouteGroup, String>, JpaSpecificationExecutor<MockRouteGroup> {
}
@@ -75,6 +75,7 @@ public class MockRouteService {
route.setName(updated.getName()); route.setName(updated.getName());
route.setDescription(updated.getDescription()); route.setDescription(updated.getDescription());
route.setRequestScript(updated.getRequestScript()); route.setRequestScript(updated.getRequestScript());
route.setGroup(updated.getGroup());
if (!route.getPathPattern().startsWith("/")) { if (!route.getPathPattern().startsWith("/")) {
route.setPathPattern("/" + route.getPathPattern()); route.setPathPattern("/" + route.getPathPattern());
} }
@@ -0,0 +1,77 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/default_layout}">
<body>
<section layout:fragment="contentFragment">
<div class="container-fluid mt-2">
<form name="groupForm" id="groupForm"
th:action="${group.id == null}?@{/mgmt/routes/group/register.do}:@{/mgmt/routes/group/update.do}"
th:object='${group}' method="post">
<div class="card col-md">
<div class="card-header">
<h3 th:if="${group.id == null}">API 그룹 등록</h3>
<h3 th:if="${group.id != null}">API 그룹 수정</h3>
</div>
<div class="card-body">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<input type="hidden" name="id" th:value="${group.id}" th:if="${group.id != null}">
<div class="form-floating mt-2">
<input type="text" class="form-control"
th:classappend="${#fields.hasErrors('id')}? 'is-invalid'"
th:field="*{id}" required th:readonly="${group.id != null}"/>
<label for="id" class="form-label must">그룹 ID</label>
<th:block th:each="err : ${#fields.errors('id')}">
<div th:text="${err}" class="invalid-feedback"></div>
</th:block>
</div>
<div class="form-floating mt-2">
<input type="text" class="form-control"
th:classappend="${#fields.hasErrors('name')}? 'is-invalid'"
th:field="*{name}" required/>
<label for="name" class="form-label must">그룹명</label>
<th:block th:each="err : ${#fields.errors('name')}">
<div th:text="${err}" class="invalid-feedback"></div>
</th:block>
</div>
</div>
<div class="card-footer">
<button type="button" class="btn btn-primary btn_list">
<i class="fa-solid fa-list"></i> 목록
</button>
<button type="button" class="btn btn-primary btn_register">
<i class="fa-sharp fa-solid fa-floppy-disk"></i> 저장
</button>
</div>
</div>
</form>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script>
$(function () {
let fnRegister = document.querySelector(".btn_register");
let fnList = document.querySelector(".btn_list");
fnRegister.addEventListener("click", function (event) {
let form = document.getElementById("groupForm");
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
} else {
form.submit();
}
form.classList.add('was-validated');
});
fnList.addEventListener("click", function () {
location.href = "[[@{/mgmt/routes/group/list_view.do}]]";
});
});
</script>
</th:block>
</html>
@@ -0,0 +1,145 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/default_layout}">
<body>
<section layout:fragment="contentFragment">
<div class="container-fluid">
<div class="card mt-4">
<div class="card-header">
<h5><span style="font-weight: bold">API 그룹 목록</span></h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between">
<form role="form" class="row g-3" name="searchForm" th:action="@{/mgmt/routes/group/list_view.do}" method="get">
<input name="page" type="hidden" value="1"/>
<div class="col-auto">
<button type="button" class="btn btn-primary btn_delete">
<i class="fa-sharp fa-solid fa-trash"></i> [[#{title.delete}]]
</button>
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary btn_add">
<i class="fa-sharp fa-solid fa-plus"></i> [[#{button.create}]]
</button>
</div>
</form>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<form name="listForm">
<input name="id" type="hidden"/>
<input name="checkedIdForDel" type="hidden"/>
<input id="csrf" type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<table class="table table-hover pt-table">
<colgroup>
<col style="width: 5%;">
<col style="width: 5%;">
<col style="width: 20%;">
<col>
<col style="width: 12%;">
</colgroup>
<thead>
<tr>
<th><div class="text-center">번호</div></th>
<th>
<div class="text-center">
<input type="checkbox" name="checkAll" class="form-check-input" onclick="fncCheckAll()" th:title="#{input.selectAll.title}">
</div>
</th>
<th><div class="text-center">ID</div></th>
<th><div class="text-center">이름</div></th>
<th><div class="text-center">비고</div></th>
</tr>
</thead>
<tbody>
<tr th:each="row, status : ${page.content}">
<td class="text-center" th:text="${page.getNumber() * page.getSize() + status.count}"></td>
<td>
<div class="d-flex justify-content-center">
<input type="checkbox" name="checkField" class="form-check-input" title="선택"/>
<input name="checkId" type="hidden" th:value="${row.id}"/>
</div>
</td>
<td>
<span th:text="${row.id}"></span>
</td>
<td>
<span th:text="${row.name}"></span>
</td>
<td>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-primary btn-sm me-2" th:onclick="fnSelectItem([[${row.id}]])" th:value="#{button.update}">
<i class="fa-sharp fa-solid fa-edit"></i> [[#{button.update}]]
</button>
</div>
</td>
</tr>
<tr th:if="${page.content.size() == 0}">
<td colspan="5" th:text="#{common.nodata.msg}"></td>
</tr>
</tbody>
</table>
</form>
</div>
<div class="card-footer">
<div th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
</div>
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script>
function fn_select_page(pageNo) {
document.searchForm.page.value = pageNo;
document.searchForm.submit();
}
function fnSelectItem(id) {
location.href = "[[@{/mgmt/routes/group/update_view.do}]]" + "?id=" + id;
}
$(function () {
let fnDelete = document.querySelector(".btn_delete");
let fnAdd = document.querySelector(".btn_add");
fnDelete.addEventListener("click", function () {
var checkField = document.listForm.checkField;
var id = document.listForm.checkId;
var checkedIds = "";
var checkedCount = 0;
if (checkField) {
if (checkField.length > 1) {
for (var i = 0; i < checkField.length; i++) {
if (checkField[i].checked) {
checkedIds += ((checkedCount == 0 ? "" : ",") + id[i].value);
checkedCount++;
}
}
} else {
if (checkField.checked) {
checkedIds = id.value;
}
}
}
if (checkedIds.length > 0) {
if (confirm("[[#{common.delete.msg}]]")) {
document.listForm.checkedIdForDel.value = checkedIds;
document.listForm.method = 'POST';
document.listForm.action = '[[@{/mgmt/routes/group/delete.do}]]';
document.listForm.submit();
}
}
});
fnAdd.addEventListener("click", function () {
location.href = '[[@{/mgmt/routes/group/create_view.do}]]';
});
});
</script>
</th:block>
</html>
@@ -36,6 +36,18 @@
<div th:text="${err}" class="invalid-feedback"></div> <div th:text="${err}" class="invalid-feedback"></div>
</th:block> </th:block>
</div> </div>
<div class="form-floating mt-2">
<select class="form-select" name="group" th:value="${mockRoute.group}">
<option value="" th:selected="${#strings.isEmpty(mockRoute.group)}">선택</option>
<!-- 그룹 목록 반복 -->
<option th:each="group : ${groupList}"
th:value="${group.id}"
th:text="${group.name}"
th:selected="${mockRoute.group} == ${group.id}">
</option>
</select>
<label for="group" class="form-label must" >그룹</label>
</div>
<div class="d-flex align-items-center mt-2"> <div class="d-flex align-items-center mt-2">
<div class="form-floating col-md-1 me-2"> <div class="form-floating col-md-1 me-2">
<select name="method" class="form-select" th:if="${mockRoute.id == null}" <select name="method" class="form-select" th:if="${mockRoute.id == null}"
@@ -16,6 +16,18 @@
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<form role="form" class="row g-3" name="searchForm" th:action="@{/mgmt/routes/list_view.do}" method="get" th:object="${modelSearch}"> <form role="form" class="row g-3" name="searchForm" th:action="@{/mgmt/routes/list_view.do}" method="get" th:object="${modelSearch}">
<input name="page" type="hidden" value="1"/> <input name="page" type="hidden" value="1"/>
<div class="col-auto">
<select class="form-select" name="group" th:value="${modelSearch.group}">
<option value="" th:selected="${#strings.isEmpty(modelSearch.group)}">선택</option>
<!-- 그룹 목록 반복 -->
<option th:each="group : ${groupList}"
th:value="${group.id}"
th:text="${group.name}"
th:selected="${modelSearch.group} == ${group.id}">
</option>
</select>
</div>
<div class="col-auto"> <div class="col-auto">
<input class="form-control" name="name" placeholder="이름" type="text" size="35" th:title="#{title.search}+ '' + #{input.input}" <input class="form-control" name="name" placeholder="이름" type="text" size="35" th:title="#{title.search}+ '' + #{input.input}"
th:value="${modelSearch.name}" th:value="${modelSearch.name}"
@@ -31,6 +43,9 @@
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-primary btn_add"><i class="fa-sharp fa-solid fa-plus"></i> [[#{button.create}]]</button><!-- 등록 --> <button type="button" class="btn btn-primary btn_add"><i class="fa-sharp fa-solid fa-plus"></i> [[#{button.create}]]</button><!-- 등록 -->
</div> </div>
<div class="col-auto">
<button type="button" class="btn btn-primary btn_add_group"><i class="fa-sharp fa-solid fa-plus"></i>그룹 관리</button><!-- 등록 -->
</div>
</form> </form>
</div> </div>
</div> </div>
@@ -43,6 +58,7 @@
<input id="csrf" type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> <input id="csrf" type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<table class="table table-hover pt-table"> <table class="table table-hover pt-table">
<colgroup> <colgroup>
<col style="width: 5%;">
<col style="width: 5%;"> <col style="width: 5%;">
<col style="width: 5%;"> <col style="width: 5%;">
<col style="width: 10%;"> <col style="width: 10%;">
@@ -58,6 +74,7 @@
<input type="checkbox" name="checkAll" class="form-check-input" onclick="fncCheckAll()" th:title="#{input.selectAll.title}"> <input type="checkbox" name="checkAll" class="form-check-input" onclick="fncCheckAll()" th:title="#{input.selectAll.title}">
</div> </div>
</th> </th>
<th><div class="text-center">group</div></th>
<th><div class="text-center">method</div></th> <th><div class="text-center">method</div></th>
<th><div class="text-center">path</div></th> <th><div class="text-center">path</div></th>
<th><div class="text-center">이름</div></th> <th><div class="text-center">이름</div></th>
@@ -73,6 +90,14 @@
<input name="checkId" type="hidden" th:value="${row.id}"/> <input name="checkId" type="hidden" th:value="${row.id}"/>
</div> </div>
</td> </td>
<td>
<div class="d-flex justify-content-center">
<span th:each="group : ${groupList}"
th:if="${group.id} == ${row.group}"
th:text="${group.name}">그룹 미지정</span>
</div>
</td>
<td> <td>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<span th:text="${row.method}"></span> <span th:text="${row.method}"></span>
@@ -138,6 +163,7 @@
let fnSearch = document.querySelector(".btn_search"); let fnSearch = document.querySelector(".btn_search");
let fnDelete = document.querySelector(".btn_delete"); let fnDelete = document.querySelector(".btn_delete");
let fnAdd = document.querySelector(".btn_add"); let fnAdd = document.querySelector(".btn_add");
let fnAddGroup = document.querySelector(".btn_add_group");
fnSearch.addEventListener("click", function () { fnSearch.addEventListener("click", function () {
document.searchForm.page.value = 1; document.searchForm.page.value = 1;
@@ -176,6 +202,10 @@
fnAdd.addEventListener("click", function () { fnAdd.addEventListener("click", function () {
location.href = '[[@{/mgmt/routes/create_view.do}]]'; location.href = '[[@{/mgmt/routes/create_view.do}]]';
}); });
fnAddGroup.addEventListener("click", function () {
location.href = '[[@{/mgmt/routes/group/list_view.do}]]';
});
}) })
</script> </script>