package com.snp.batch.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.snp.batch.global.dto.bypass.BypassAccountResponse; import com.snp.batch.global.dto.bypass.BypassAccountUpdateRequest; import com.snp.batch.global.dto.bypass.ServiceIpDto; import com.snp.batch.global.model.AccountStatus; import com.snp.batch.global.model.BypassApiAccount; import com.snp.batch.global.model.BypassApiServiceIp; import com.snp.batch.global.repository.BypassApiAccountRepository; import com.snp.batch.global.repository.BypassApiServiceIpRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.security.SecureRandom; import java.time.LocalDate; import java.util.List; @Slf4j @Service @RequiredArgsConstructor public class BypassApiAccountService { private static final String USERNAME_PREFIX = "bypass_"; private static final int USERNAME_RANDOM_LENGTH = 8; private static final int PASSWORD_LENGTH = 16; private static final String ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyz0123456789"; private static final String PASSWORD_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*"; private static final SecureRandom RANDOM = new SecureRandom(); private final BypassApiAccountRepository accountRepository; private final BypassApiServiceIpRepository serviceIpRepository; private final PasswordEncoder passwordEncoder; @Transactional public BypassAccountResponse createAccount(String displayName, String organization, String projectName, String email, String phone, LocalDate accessStartDate, LocalDate accessEndDate) { String rawUsername = generateUsername(); String rawPassword = generatePassword(); BypassApiAccount account = BypassApiAccount.builder() .username(rawUsername) .passwordHash(passwordEncoder.encode(rawPassword)) .displayName(displayName) .organization(organization) .projectName(projectName) .email(email) .phone(phone) .status(AccountStatus.ACTIVE) .accessStartDate(accessStartDate) .accessEndDate(accessEndDate) .build(); BypassApiAccount saved = accountRepository.save(account); log.info("Bypass API 계정 생성: username={}", rawUsername); return toResponse(saved, rawPassword); } @Transactional(readOnly = true) public Page getAccounts(String status, int page, int size) { PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); Page accounts; if (status != null && !status.isBlank()) { accounts = accountRepository.findByStatus(AccountStatus.valueOf(status), pageRequest); } else { accounts = accountRepository.findAll(pageRequest); } return accounts.map(this::toResponse); } @Transactional(readOnly = true) public BypassAccountResponse getAccount(Long id) { return toResponse(findOrThrow(id)); } @Transactional public BypassAccountResponse updateAccount(Long id, BypassAccountUpdateRequest request) { BypassApiAccount account = findOrThrow(id); if (request.getDisplayName() != null) account.setDisplayName(request.getDisplayName()); if (request.getOrganization() != null) account.setOrganization(request.getOrganization()); if (request.getEmail() != null) account.setEmail(request.getEmail()); if (request.getPhone() != null) account.setPhone(request.getPhone()); if (request.getStatus() != null) account.setStatus(AccountStatus.valueOf(request.getStatus())); if (request.getAccessStartDate() != null) account.setAccessStartDate(request.getAccessStartDate()); if (request.getAccessEndDate() != null) account.setAccessEndDate(request.getAccessEndDate()); return toResponse(accountRepository.save(account)); } @Transactional public void deleteAccount(Long id) { BypassApiAccount account = findOrThrow(id); accountRepository.delete(account); log.info("Bypass API 계정 삭제: username={}", account.getUsername()); } @Transactional public BypassAccountResponse resetPassword(Long id) { BypassApiAccount account = findOrThrow(id); String rawPassword = generatePassword(); account.setPasswordHash(passwordEncoder.encode(rawPassword)); accountRepository.save(account); log.info("Bypass API 비밀번호 재설정: username={}", account.getUsername()); return toResponse(account, rawPassword); } private BypassApiAccount findOrThrow(Long id) { return accountRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("계정을 찾을 수 없습니다: " + id)); } private String generateUsername() { for (int i = 0; i < 10; i++) { StringBuilder sb = new StringBuilder(USERNAME_PREFIX); for (int j = 0; j < USERNAME_RANDOM_LENGTH; j++) { sb.append(ALPHANUMERIC.charAt(RANDOM.nextInt(ALPHANUMERIC.length()))); } String candidate = sb.toString(); if (!accountRepository.existsByUsername(candidate)) { return candidate; } } throw new IllegalStateException("Username 생성 실패: 10회 시도 초과"); } private String generatePassword() { StringBuilder sb = new StringBuilder(PASSWORD_LENGTH); for (int i = 0; i < PASSWORD_LENGTH; i++) { sb.append(PASSWORD_CHARS.charAt(RANDOM.nextInt(PASSWORD_CHARS.length()))); } return sb.toString(); } @Transactional(readOnly = true) public List getServiceIps(Long accountId) { findOrThrow(accountId); return serviceIpRepository.findByAccountId(accountId).stream() .map(ip -> ServiceIpDto.builder() .ip(ip.getIpAddress()) .purpose(ip.getPurpose()) .description(ip.getDescription()) .expectedCallVolume(ip.getExpectedCallVolume()) .build()) .toList(); } @Transactional public ServiceIpDto addServiceIp(Long accountId, ServiceIpDto dto) { BypassApiAccount account = findOrThrow(accountId); BypassApiServiceIp saved = serviceIpRepository.save(BypassApiServiceIp.builder() .account(account) .ipAddress(dto.getIp()) .purpose(dto.getPurpose() != null ? dto.getPurpose() : "ETC") .description(dto.getDescription()) .expectedCallVolume(dto.getExpectedCallVolume()) .build()); return ServiceIpDto.builder() .ip(saved.getIpAddress()) .purpose(saved.getPurpose()) .description(saved.getDescription()) .expectedCallVolume(saved.getExpectedCallVolume()) .build(); } @Transactional public void deleteServiceIp(Long accountId, Long ipId) { findOrThrow(accountId); serviceIpRepository.deleteById(ipId); } private String getServiceIpsJson(Long accountId) { List ips = serviceIpRepository.findByAccountId(accountId); if (ips.isEmpty()) return null; try { ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(ips.stream().map(ip -> ServiceIpDto.builder() .ip(ip.getIpAddress()) .purpose(ip.getPurpose()) .description(ip.getDescription()) .expectedCallVolume(ip.getExpectedCallVolume()) .build() ).toList()); } catch (Exception e) { return null; } } private BypassAccountResponse toResponse(BypassApiAccount account, String plainPassword) { return BypassAccountResponse.builder() .id(account.getId()) .username(account.getUsername()) .displayName(account.getDisplayName()) .organization(account.getOrganization()) .projectName(account.getProjectName()) .email(account.getEmail()) .phone(account.getPhone()) .status(account.getStatus().name()) .accessStartDate(account.getAccessStartDate()) .accessEndDate(account.getAccessEndDate()) .createdAt(account.getCreatedAt()) .updatedAt(account.getUpdatedAt()) .plainPassword(plainPassword) .serviceIps(account.getId() != null ? getServiceIpsJson(account.getId()) : null) .build(); } private BypassAccountResponse toResponse(BypassApiAccount account) { return toResponse(account, null); } }