package com.cloudhandson.ddsbackoffice.config; import java.time.Duration; import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Pattern; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.ConstructorBinding; @ConfigurationProperties(prefix = "dds") public record DdsProperties( String dbUrl, Duration queryTimeout, String pgObject, String myObject, String vectorObject, Map users, Map objectMappings ) { private static final Pattern OBJECT_NAME = Pattern.compile( "[A-Za-z][A-Za-z0-9_$#]*(\\.[A-Za-z][A-Za-z0-9_$#]*)*" ); @ConstructorBinding public DdsProperties { dbUrl = dbUrl == null ? "" : dbUrl.trim(); queryTimeout = queryTimeout == null ? Duration.ofSeconds(10) : queryTimeout; pgObject = normalizeObject(pgObject, "ADMIN.V_DDS_CUSTOMERS_PG"); myObject = normalizeObject(myObject, "ADMIN.V_DDS_CUSTOMERS_MY"); vectorObject = normalizeObject(vectorObject, "ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS"); var normalizedUsers = new LinkedHashMap(); if (users != null) { users.forEach((key, value) -> { if (key != null && value != null) { normalizedUsers.put(key.trim().toLowerCase(), value); } }); } users = Map.copyOf(normalizedUsers); var normalizedMappings = new LinkedHashMap(); if (objectMappings != null) { objectMappings.forEach((key, value) -> { if (key != null && value != null && !key.isBlank() && !value.isBlank()) { normalizedMappings.put(key.trim().toUpperCase(), normalizeObject(value, "")); } }); } objectMappings = Map.copyOf(normalizedMappings); } /** * Compatibility constructor for small unit tests and older local configs. * The DDS vector view has a safe default and can still be overridden by * {@code DDS_BACKOFFICE_VECTOR_OBJECT}. */ public DdsProperties( String dbUrl, Duration queryTimeout, String pgObject, String myObject, Map users ) { this(dbUrl, queryTimeout, pgObject, myObject, "ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS", users, Map.of( "CB_VECTOR_SEARCH_DOCUMENTS", "ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS")); } public DdsProperties( String dbUrl, Duration queryTimeout, String pgObject, String myObject, String vectorObject, Map users ) { this(dbUrl, queryTimeout, pgObject, myObject, vectorObject, users, Map.of()); } public String objectFor(String sourceKey) { if ("PG".equalsIgnoreCase(sourceKey)) { return pgObject; } if ("MY".equalsIgnoreCase(sourceKey)) { return myObject; } throw new IllegalArgumentException("지원하지 않는 DDS 데이터 소스입니다."); } private static String normalizeObject(String value, String fallback) { var candidate = value == null || value.isBlank() ? fallback : value.trim(); if (!OBJECT_NAME.matcher(candidate).matches()) { throw new IllegalArgumentException("DDS 보호 객체 이름은 스키마.객체 형식이어야 합니다."); } return candidate.toUpperCase(); } public record User( String label, String description, String username, String password, long applicationUserId, String dataRole ) { /** * Keep the six-field constructor as the configuration binding target. * The four-field overload below exists only for the small query-service * tests and older local callers; without an explicit binding marker, * Spring Boot can select that overload and silently leave the application * user/data-role mapping at its default values. */ @ConstructorBinding public User { } public User(String label, String description, String username, String password) { this(label, description, username, password, 0L, ""); } public boolean configured() { return username != null && !username.isBlank() && password != null && !password.isBlank(); } public boolean mapped() { return applicationUserId > 0 && dataRole != null && !dataRole.isBlank(); } } }