feat: prepare ACS deployment

This commit is contained in:
unknown
2026-07-10 15:18:29 +09:00
parent da0d35ae7f
commit 01d48fe808
25 changed files with 1070 additions and 83 deletions

View File

@@ -29,13 +29,18 @@ public class DataSeeder implements CommandLineRunner {
private final ZoneRepository zoneRepository;
private final PasswordEncoder passwordEncoder;
private static final String DEFAULT_PASSWORD = "ChangeMe123!";
// 적용(운영 반영) 전까지 신속한 테스트를 위한 단축 계정. dev(비-prod)에서만 시드된다.
// 운영 전환 시 이 시드는 무시되고 Flyway/CSV 시더가 실제 계정을 관리한다.
private static final String TEST_PASSWORD = "1";
@Override
public void run(String... args) {
seedUser("admin", "관리자", "IT운영팀", Set.of(RoleType.ADMIN));
seedUser("security", "보안담당", "보안팀", Set.of(RoleType.SECURITY));
seedUser("host", "홍길동", "개발1팀", Set.of(RoleType.HOST));
seedUser("a", "관리자", "IT운영팀", Set.of(RoleType.ADMIN));
seedUser("s", "보안담당", "보안팀", Set.of(RoleType.SECURITY));
seedUser("h", "홍길동", "개발1팀", Set.of(RoleType.HOST));
seedZone("LOBBY", "로비", 1);
seedZone("OFFICE", "사무공간", 2);
@@ -48,14 +53,14 @@ public class DataSeeder implements CommandLineRunner {
}
User user = new User();
user.setUsername(username);
user.setPasswordHash(passwordEncoder.encode(DEFAULT_PASSWORD));
user.setPasswordHash(passwordEncoder.encode(TEST_PASSWORD));
user.setFullName(fullName);
user.setDepartment(department);
user.setEmail(username + "@itcenter.local");
user.setRoles(roles);
user.setMustChangePassword(true);
user.setMustChangePassword(false); // 테스트 편의: 최초 로그인 시 비번변경 강제하지 않음
userRepository.save(user);
log.info("[seed] user '{}' created (default password: {})", username, DEFAULT_PASSWORD);
log.info("[seed] test user '{}' created (password: {})", username, TEST_PASSWORD);
}
private void seedZone(String code, String name, int level) {

View File

@@ -103,6 +103,8 @@ public class AuthController {
principal.getId(),
principal.getUsername(),
principal.getFullName(),
principal.getDepartment(),
principal.getEmail(),
roles,
principal.isMustChangePassword());
}

View File

@@ -6,6 +6,7 @@ import com.itcenter.acs.dto.VisitRequestCreateRequest;
import com.itcenter.acs.dto.VisitRequestResponse;
import com.itcenter.acs.entity.VisitRequest;
import com.itcenter.acs.security.SecurityUtils;
import com.itcenter.acs.service.ApprovalService;
import com.itcenter.acs.service.ExcelImportService;
import com.itcenter.acs.service.VisitRequestService;
import jakarta.validation.Valid;
@@ -31,13 +32,27 @@ public class VisitRequestController {
private final VisitRequestService visitRequestService;
private final ExcelImportService excelImportService;
private final ApprovalService approvalService;
/** Create a single pre-registration; current user becomes the host. */
/**
* Create pre-registration(s); current user becomes the host. One request is issued per
* selected server room, so the response may contain more than one (e.g. 4층+5층전산실 → 2건).
* When the registrant already holds approval authority (ADMIN/SECURITY), each request is
* self-approved immediately (QR issued) — HOST registrations stay PENDING.
*/
@PostMapping
public ResponseEntity<ApiResponse<VisitRequestResponse>> create(
public ResponseEntity<ApiResponse<List<VisitRequestResponse>>> create(
@Valid @RequestBody VisitRequestCreateRequest request) {
VisitRequest created = visitRequestService.create(request, SecurityUtils.currentUserId());
return ResponseEntity.ok(ApiResponse.success(VisitRequestResponse.from(created)));
Long userId = SecurityUtils.currentUserId();
List<VisitRequest> created = visitRequestService.createRequests(request, userId);
if (SecurityUtils.hasRole("ADMIN") || SecurityUtils.hasRole("SECURITY")) {
List<VisitRequest> approved = new java.util.ArrayList<>(created.size());
for (VisitRequest vr : created) {
approved.add(approvalService.approve(vr.getId(), userId, "본인 등록 자동승인"));
}
created = approved;
}
return ResponseEntity.ok(ApiResponse.success(toResponses(created)));
}
/** ADMIN/SECURITY see all; HOST sees only their own requests. */

View File

@@ -11,6 +11,8 @@ public class CurrentUserResponse {
private Long id;
private String username;
private String fullName;
private String department;
private String email;
private Set<String> roles;
private boolean mustChangePassword;
}

View File

@@ -1,11 +1,11 @@
package com.itcenter.acs.dto;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class VisitRequestCreateRequest {
@@ -20,12 +20,40 @@ public class VisitRequestCreateRequest {
private String email;
private String vehicleNo;
/** Access zone label (fixed list value or "기타" free text). */
private String zoneName;
/**
* Server rooms (전산실) selected via checkboxes, e.g. ["4층전산실", "5층전산실"].
* Each selected room becomes its own visit request / QR pass.
*/
private List<String> serverRooms;
/**
* Optional detail room (콤보박스, 기타 선택 시 자유 입력값). Auxiliary info — carried
* alongside the server room on the zone label but never issues its own QR. When no
* server room is selected, this becomes the sole access zone.
*/
private String roomZone;
@NotBlank(message = "출입 목적을 입력하세요.")
private String purpose;
/** 작업명 — optional concrete task detail, stored separately from purpose. */
private String workName;
// 출입통제담당자(본인) — 웹은 서버가 로그인 사용자로 채움. 엑셀은 파일값 사용.
private String controlName;
private String controlTeam;
private String controlContact;
// 현장감시자1(고정) — 웹은 서버가 고정 상수로 채움. 엑셀은 파일값(비면 고정).
private String watcher1Name;
private String watcher1Team;
private String watcher1Contact;
// 현장감시자2 — 담당자가 입력.
private String watcher2Name;
private String watcher2Team;
private String watcher2Contact;
@NotNull(message = "방문 시작 일시를 입력하세요.")
private LocalDateTime visitFrom;

View File

@@ -17,6 +17,16 @@ public class VisitRequestResponse {
private String hostDepartment;
private String zoneName;
private String purpose;
private String workName;
private String controlName;
private String controlTeam;
private String controlContact;
private String watcher1Name;
private String watcher1Team;
private String watcher1Contact;
private String watcher2Name;
private String watcher2Team;
private String watcher2Contact;
private LocalDateTime visitFrom;
private LocalDateTime visitTo;
private String status;
@@ -35,6 +45,16 @@ public class VisitRequestResponse {
r.hostDepartment = vr.getHost().getDepartment();
r.zoneName = vr.getZoneName();
r.purpose = vr.getPurpose();
r.workName = vr.getWorkName();
r.controlName = vr.getControlName();
r.controlTeam = vr.getControlTeam();
r.controlContact = vr.getControlContact();
r.watcher1Name = vr.getWatcher1Name();
r.watcher1Team = vr.getWatcher1Team();
r.watcher1Contact = vr.getWatcher1Contact();
r.watcher2Name = vr.getWatcher2Name();
r.watcher2Team = vr.getWatcher2Team();
r.watcher2Contact = vr.getWatcher2Contact();
r.visitFrom = vr.getVisitFrom();
r.visitTo = vr.getVisitTo();
r.status = vr.getStatus().name();

View File

@@ -46,6 +46,34 @@ public class VisitRequest extends BaseEntity {
@Column(nullable = false, length = 255)
private String purpose;
/** 작업명 — concrete task detail, kept separate from the simple purpose category. */
@Column(name = "work_name", length = 255)
private String workName;
// ===== 출입통제담당자 (본인 = 등록한 내부 직원) 스냅샷 =====
@Column(name = "control_name", length = 80)
private String controlName;
@Column(name = "control_team", length = 80)
private String controlTeam;
@Column(name = "control_contact", length = 60)
private String controlContact;
// ===== 현장감시자1 (고정) =====
@Column(name = "watcher1_name", length = 80)
private String watcher1Name;
@Column(name = "watcher1_team", length = 80)
private String watcher1Team;
@Column(name = "watcher1_contact", length = 60)
private String watcher1Contact;
// ===== 현장감시자2 (담당자가 입력) =====
@Column(name = "watcher2_name", length = 80)
private String watcher2Name;
@Column(name = "watcher2_team", length = 80)
private String watcher2Team;
@Column(name = "watcher2_contact", length = 60)
private String watcher2Contact;
@Column(nullable = false)
private LocalDateTime visitFrom;

View File

@@ -21,6 +21,8 @@ public class UserPrincipal implements UserDetails {
private final String username;
private final String password;
private final String fullName;
private final String department;
private final String email;
private final boolean mustChangePassword;
private final boolean enabled;
private final boolean locked;
@@ -31,6 +33,8 @@ public class UserPrincipal implements UserDetails {
this.username = user.getUsername();
this.password = user.getPasswordHash();
this.fullName = user.getFullName();
this.department = user.getDepartment();
this.email = user.getEmail();
this.mustChangePassword = user.isMustChangePassword();
this.enabled = user.isEnabled();
this.locked = user.isLocked();

View File

@@ -15,16 +15,24 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
/**
* Bulk-imports visit requests from an .xlsx file.
* Columns (row 1 = header, skipped):
* 0 visitorName | 1 company | 2 contact | 3 email | 4 vehicleNo
* 5 zoneName | 6 purpose | 7 visitFrom | 8 visitTo
* Dates accept Excel date cells or "yyyy-MM-dd HH:mm" / "yyyy-MM-dd" text.
* Bulk-imports visit requests from the corporate 방문자명단 .xlsx template
* (itcas_visitor_template.xlsx). The data sheet is "방문자명단"; the first two rows are
* group + column headers and are skipped. Columns (0-based):
* 0 순번 | 1 출입목적 | 2 작업명 | 3 장소 | 4 출입일자 | 5 출입시간 |
* 6 이름 | 7 소속 | 8 연락처(휴대전화) | 9 차량번호 |
* 10 직원명 | 11 담당팀명 | 12 연락처 (출입통제담당자) |
* 13 이름 | 14 소속 | 15 연락처 (현장감시자1) |
* 16 이름 | 17 소속 | 18 연락처 (현장감시자2)
* Each row is one 장소 → one request/QR ("4층전산실과 5층전산실은 개별 행으로 작성"). Only the
* start date+time are captured; visitTo defaults to 23:59:59 of that day (actual entry/exit
* are tracked by check-in/out). 출입일자 accepts Excel date cells or "yyyy.M.d(요일)" /
* "yyyy-MM-dd" text; 출입시간 accepts Excel time cells, "HH:mm" text, or a day fraction.
*/
@Service
@RequiredArgsConstructor
@@ -32,7 +40,9 @@ public class ExcelImportService {
private final VisitRequestService visitRequestService;
private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd[ HH:mm]");
private static final String DATA_SHEET = "방문자명단";
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-M-d");
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("H:mm[:ss]");
// Not @Transactional: each row imports in its own transaction (create() is
// @Transactional), so a duplicate/invalid row fails independently without
@@ -42,10 +52,13 @@ public class ExcelImportService {
int dataRows = 0;
try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) {
Sheet sheet = workbook.getSheetAt(0);
Sheet sheet = workbook.getSheet(DATA_SHEET);
if (sheet == null) {
sheet = workbook.getSheetAt(0);
}
for (Row row : sheet) {
if (row.getRowNum() == 0) {
continue; // header
if (row.getRowNum() <= 1) {
continue; // 그룹 헤더 + 컬럼 헤더
}
if (isEmptyRow(row)) {
continue;
@@ -53,7 +66,7 @@ public class ExcelImportService {
dataRows++;
try {
VisitRequestCreateRequest req = parseRow(row);
visitRequestService.create(req, hostUserId);
visitRequestService.createRequests(req, hostUserId);
result.setSuccessCount(result.getSuccessCount() + 1);
} catch (Exception e) {
result.getErrors().add("" + (row.getRowNum() + 1) + ": " + e.getMessage());
@@ -66,20 +79,57 @@ public class ExcelImportService {
private VisitRequestCreateRequest parseRow(Row row) {
VisitRequestCreateRequest req = new VisitRequestCreateRequest();
req.setVisitorName(requireString(row.getCell(0), "방문자 이름"));
req.setCompany(getString(row.getCell(1)));
req.setContact(requireString(row.getCell(2), "연락처"));
req.setEmail(getString(row.getCell(3)));
req.setVehicleNo(getString(row.getCell(4)));
req.setZoneName(getString(row.getCell(5)));
req.setPurpose(requireString(row.getCell(6), "출입 목적"));
req.setVisitFrom(requireDateTime(row.getCell(7), "출입 일시"));
req.setVisitTo(requireDateTime(row.getCell(8), "퇴실 일시"));
req.setPurpose(requireString(row.getCell(1), "출입목적"));
req.setWorkName(getString(row.getCell(2)));
req.setServerRooms(parseServerRooms(requireString(row.getCell(3), "장소")));
LocalDate date = requireDate(row.getCell(4), "출입일자");
LocalTime time = parseTime(row.getCell(5));
req.setVisitFrom(date.atTime(time != null ? time : LocalTime.MIDNIGHT));
// 신청 시엔 시작만 입력받고, 종료(퇴실)는 당일 마감으로 둔다 (실제 입·퇴장은 체크인/아웃에서 관리).
req.setVisitTo(date.atTime(LocalTime.of(23, 59, 59)));
req.setVisitorName(requireString(row.getCell(6), "이름"));
req.setCompany(getString(row.getCell(7)));
req.setContact(requireString(row.getCell(8), "연락처"));
req.setVehicleNo(getString(row.getCell(9)));
// 출입통제담당자 (비면 서비스가 업로드 사용자로 채움)
req.setControlName(getString(row.getCell(10)));
req.setControlTeam(getString(row.getCell(11)));
req.setControlContact(getString(row.getCell(12)));
// 현장감시자1 (비면 서비스가 고정값으로 채움)
req.setWatcher1Name(getString(row.getCell(13)));
req.setWatcher1Team(getString(row.getCell(14)));
req.setWatcher1Contact(getString(row.getCell(15)));
// 현장감시자2
req.setWatcher2Name(getString(row.getCell(16)));
req.setWatcher2Team(getString(row.getCell(17)));
req.setWatcher2Contact(getString(row.getCell(18)));
return req;
}
/** Splits the 장소 cell into individual zones on comma / semicolon / slash (usually one). */
private List<String> parseServerRooms(String raw) {
if (raw == null || raw.isBlank()) {
return List.of();
}
List<String> rooms = new ArrayList<>();
for (String part : raw.split("[,;/]")) {
String trimmed = part.trim();
if (!trimmed.isEmpty()) {
rooms.add(trimmed);
}
}
return rooms;
}
// A pre-numbered but otherwise blank template row (순번 filled, rest empty) is skipped;
// 순번(0) is ignored so those rows don't count as data.
private static final int[] KEY_COLS = {1, 3, 4, 6, 8};
private boolean isEmptyRow(Row row) {
for (int c = 0; c <= 8; c++) {
for (int c : KEY_COLS) {
String v = getString(row.getCell(c));
if (v != null && !v.isBlank()) {
return false;
@@ -118,21 +168,62 @@ public class ExcelImportService {
return v;
}
private LocalDateTime requireDateTime(Cell cell, String field) {
/** Parses 출입일자: Excel date cell, or text like "2026.7.8(수)" / "2026-07-08" / "2026.07.08". */
private LocalDate requireDate(Cell cell, String field) {
if (cell == null) {
throw new IllegalArgumentException(field + "은(는) 필수입니다.");
}
if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
return cell.getLocalDateTimeCellValue();
return cell.getLocalDateTimeCellValue().toLocalDate();
}
String text = requireString(cell, field);
String norm = text;
int paren = norm.indexOf('(');
if (paren >= 0) {
norm = norm.substring(0, paren); // "(요일)" 제거
}
norm = norm.trim().replace('.', '-').replace('/', '-').replace(" ", "");
norm = norm.replaceAll("-{2,}", "-").replaceAll("-+$", "");
try {
if (text.length() <= 10) {
return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd")).atStartOfDay();
}
return LocalDateTime.parse(text.replace('T', ' '), DT);
return LocalDate.parse(norm, DATE_FMT);
} catch (Exception e) {
throw new IllegalArgumentException(field + " 형식이 올바르지 않습니다 (yyyy-MM-dd HH:mm): " + text);
throw new IllegalArgumentException(
field + " 형식이 올바르지 않습니다 (예: 2026-07-08 또는 2026.7.8): " + text);
}
}
/** Parses 출입시간 (optional): Excel time cell, "HH:mm" text, or a day fraction (0.625 = 15:00). */
private LocalTime parseTime(Cell cell) {
if (cell == null) {
return null;
}
if (cell.getCellType() == CellType.NUMERIC) {
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getLocalDateTimeCellValue().toLocalTime();
}
return fractionToTime(cell.getNumericCellValue());
}
String t = getString(cell);
if (t == null || t.isBlank()) {
return null;
}
t = t.trim();
try {
if (t.matches("\\d{1,2}:\\d{2}(:\\d{2})?")) {
return LocalTime.parse(t, TIME_FMT);
}
double d = Double.parseDouble(t);
return fractionToTime(d);
} catch (Exception e) {
return null; // 해석 불가한 시간 → 시작시간 미지정으로 처리
}
}
private LocalTime fractionToTime(double d) {
double frac = d - Math.floor(d);
if (frac < 0) {
return null;
}
return LocalTime.ofSecondOfDay(Math.round(frac * 86400) % 86400);
}
}

View File

@@ -52,7 +52,7 @@ public class ReportService {
List<VisitRequest> rows = visitRequestRepository
.findByVisitFromBetweenOrderByVisitFromAsc(from.atStartOfDay(), to.plusDays(1).atStartOfDay());
String[] headers = {"방문자", "회사", "연락처", "출입구역", "호스트", "출입목적", "출입일시", "퇴실일시", "상태"};
String[] headers = {"방문자", "회사", "연락처", "출입구역", "호스트", "출입목적", "작업명", "출입일시", "퇴실일시", "상태"};
try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Sheet sheet = wb.createSheet("출입기록");
@@ -77,9 +77,10 @@ public class ReportService {
put(row, 3, nv(vr.getZoneName()), widths);
put(row, 4, vr.getHost().getFullName(), widths);
put(row, 5, nv(vr.getPurpose()), widths);
put(row, 6, fmt(vr.getVisitFrom()), widths);
put(row, 7, fmt(vr.getVisitTo()), widths);
put(row, 8, STATUS_KO.getOrDefault(vr.getStatus(), vr.getStatus().name()), widths);
put(row, 6, nv(vr.getWorkName()), widths);
put(row, 7, fmt(vr.getVisitFrom()), widths);
put(row, 8, fmt(vr.getVisitTo()), widths);
put(row, 9, STATUS_KO.getOrDefault(vr.getStatus(), vr.getStatus().name()), widths);
}
// autoSizeColumn under-measures CJK text, so set widths from the content

View File

@@ -13,6 +13,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
@@ -24,15 +25,23 @@ public class VisitRequestService {
private final VisitorRepository visitorRepository;
private final UserRepository userRepository;
// 현장감시자1은 고정 인원. (실제 지정 인원이 다르면 이 상수만 수정)
private static final String FIXED_WATCHER1_NAME = "류관순";
private static final String FIXED_WATCHER1_TEAM = "IT전략국";
private static final String FIXED_WATCHER1_CONTACT = "313";
/**
* Create a pre-registration. Reuses an existing visitor (matched by name+contact)
* or creates a new one. The created request starts in PENDING.
* Create pre-registration(s). Reuses an existing visitor (matched by name+contact)
* or creates a new one, then issues one PENDING request per resolved access zone —
* so selecting two server rooms produces two requests (and, on approval, two QR passes).
*/
public VisitRequest create(VisitRequestCreateRequest req, Long hostUserId) {
public List<VisitRequest> createRequests(VisitRequestCreateRequest req, Long hostUserId) {
if (req.getVisitTo().isBefore(req.getVisitFrom())) {
throw ApiException.badRequest("방문 종료 일시가 시작 일시보다 빠를 수 없습니다.");
}
List<String> zoneNames = resolveZones(req);
User host = userRepository.findById(hostUserId)
.orElseThrow(() -> ApiException.notFound("호스트 사용자를 찾을 수 없습니다."));
@@ -46,20 +55,84 @@ public class VisitRequestService {
visitor.setVehicleNo(req.getVehicleNo());
visitor = visitorRepository.save(visitor);
if (visitRequestRepository.existsActiveDuplicate(
visitor.getId(), req.getVisitFrom(), req.getVisitTo(), req.getZoneName())) {
throw ApiException.conflict("이미 동일한 방문 신청이 존재합니다. (방문자·기간·구역 중복)");
List<VisitRequest> created = new ArrayList<>();
for (String zoneName : zoneNames) {
if (visitRequestRepository.existsActiveDuplicate(
visitor.getId(), req.getVisitFrom(), req.getVisitTo(), zoneName)) {
throw ApiException.conflict(
"이미 동일한 방문 신청이 존재합니다. (방문자·기간·구역 중복: " + zoneName + ")");
}
VisitRequest vr = new VisitRequest();
vr.setVisitor(visitor);
vr.setHost(host);
vr.setZoneName(zoneName);
vr.setPurpose(req.getPurpose());
vr.setWorkName(req.getWorkName());
vr.setVisitFrom(req.getVisitFrom());
vr.setVisitTo(req.getVisitTo());
vr.setStatus(VisitStatus.PENDING);
// 출입통제담당자 = 요청값 있으면 사용(엑셀), 없으면 본인(등록한 로그인 사용자) 정보로 채움.
vr.setControlName(orElse(req.getControlName(), host.getFullName()));
vr.setControlTeam(orElse(req.getControlTeam(), host.getDepartment()));
vr.setControlContact(orElse(req.getControlContact(), host.getEmail()));
// 현장감시자1 = 고정값(요청값 비면 고정 상수).
vr.setWatcher1Name(orElse(req.getWatcher1Name(), FIXED_WATCHER1_NAME));
vr.setWatcher1Team(orElse(req.getWatcher1Team(), FIXED_WATCHER1_TEAM));
vr.setWatcher1Contact(orElse(req.getWatcher1Contact(), FIXED_WATCHER1_CONTACT));
// 현장감시자2 = 담당자 입력값 그대로.
vr.setWatcher2Name(trimToNull(req.getWatcher2Name()));
vr.setWatcher2Team(trimToNull(req.getWatcher2Team()));
vr.setWatcher2Contact(trimToNull(req.getWatcher2Contact()));
created.add(visitRequestRepository.save(vr));
}
return created;
}
/** Trimmed value, or null when blank. */
private static String trimToNull(String v) {
if (v == null) {
return null;
}
String t = v.trim();
return t.isEmpty() ? null : t;
}
/** The trimmed value if present, otherwise the fallback. */
private static String orElse(String value, String fallback) {
String v = trimToNull(value);
return v != null ? v : fallback;
}
/**
* Resolves the access-zone labels for a request. Each selected server room (전산실) is a
* zone of its own (its own QR); the detail room (콤보박스) is auxiliary and appended to
* the label. With no server room selected, the detail room becomes the sole zone.
*/
private List<String> resolveZones(VisitRequestCreateRequest req) {
String room = req.getRoomZone() == null ? "" : req.getRoomZone().trim();
List<String> serverRooms = new ArrayList<>();
if (req.getServerRooms() != null) {
for (String s : req.getServerRooms()) {
if (s != null && !s.trim().isEmpty()) {
serverRooms.add(s.trim());
}
}
}
VisitRequest vr = new VisitRequest();
vr.setVisitor(visitor);
vr.setHost(host);
vr.setZoneName(req.getZoneName());
vr.setPurpose(req.getPurpose());
vr.setVisitFrom(req.getVisitFrom());
vr.setVisitTo(req.getVisitTo());
vr.setStatus(VisitStatus.PENDING);
return visitRequestRepository.save(vr);
List<String> zones = new ArrayList<>();
if (!serverRooms.isEmpty()) {
for (String sr : serverRooms) {
zones.add(room.isEmpty() ? sr : sr + " / " + room);
}
} else if (!room.isEmpty()) {
zones.add(room);
} else {
throw ApiException.badRequest("출입 구역을 최소 1개 이상 선택하세요.");
}
return zones;
}
@Transactional(readOnly = true)

View File

@@ -0,0 +1,11 @@
-- Visit request detail fields added after the initial schema.
ALTER TABLE visit_requests ADD COLUMN work_name VARCHAR(255);
ALTER TABLE visit_requests ADD COLUMN control_name VARCHAR(80);
ALTER TABLE visit_requests ADD COLUMN control_team VARCHAR(80);
ALTER TABLE visit_requests ADD COLUMN control_contact VARCHAR(60);
ALTER TABLE visit_requests ADD COLUMN watcher1_name VARCHAR(80);
ALTER TABLE visit_requests ADD COLUMN watcher1_team VARCHAR(80);
ALTER TABLE visit_requests ADD COLUMN watcher1_contact VARCHAR(60);
ALTER TABLE visit_requests ADD COLUMN watcher2_name VARCHAR(80);
ALTER TABLE visit_requests ADD COLUMN watcher2_team VARCHAR(80);
ALTER TABLE visit_requests ADD COLUMN watcher2_contact VARCHAR(60);