Merge remote-tracking branch 'origin/main'

# Conflicts:
#	dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/ToolScaffolder.java
#	dat-was-pro/src/main/java/io/shinhanlife/dat/mcc/biz/pro/converter/RecruitmentFeeDisclosureDataConverter.java
#	dat-was-pro/src/main/java/io/shinhanlife/dat/mcc/biz/pro/dto/RecruitmentFeeDisclosureDataRequest.java
#	dat-was-sal/Dockerfile
#	dat-was-sal/build.gradle
#	dat-was-sal/src/main/java/io/shinhanlife/dat/mcc/sal/DatWasSalApplication.java
#	dat-was-sal/src/main/resources/application-dev.yml
#	dat-was-sal/src/main/resources/application-local.yml
#	dat-was-sal/src/main/resources/application-prod.yml
#	dat-was-sal/src/main/resources/application-test.yml
#	dat-was-sal/src/main/resources/application.yml
#	dat-was-sal/src/main/resources/logback-spring.xml
#	dat-was-sal/src/main/resources/tool-service-manifest.yml
This commit is contained in:
jade
2026-09-17 17:32:18 +09:00
18 changed files with 4037 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
package io.shinhanlife.dat.lib.paging;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
/**
* @package io.shinhanlife.dat.lib.paging
* @className MciPagingUtils
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Slf4j
public final class MciPagingUtils {
private MciPagingUtils() {
}
/**
* 페이징 응답 데이터(List 필드)를 대상(target) 객체에 순차적으로 병합(append)합니다.
* 클래스 상속 계층 구조를 탐색하여 모든 List 타입 필드를 병합합니다.
*
* @param target 누적할 최종 응답 DTO
* @param src 각 페이지별 응답 DTO
*/
public static void mergeResponseData(Object target, Object src) {
if (target == null || src == null) {
return;
}
Class<?> current = target.getClass();
while (current != null && current != Object.class) {
for (Field field : current.getDeclaredFields()) {
if (List.class.isAssignableFrom(field.getType())) {
field.setAccessible(true);
try {
List<?> srcList = (List<?>) field.get(src);
if (srcList != null && !srcList.isEmpty()) {
@SuppressWarnings("unchecked")
List<Object> targetList = (List<Object>) field.get(target);
if (targetList == null) {
targetList = new ArrayList<>(srcList);
field.set(target, targetList);
} else {
if (!(targetList instanceof ArrayList)) {
targetList = new ArrayList<>(targetList);
field.set(target, targetList);
}
targetList.addAll(srcList);
}
}
} catch (Exception e) {
log.warn("[MCI Tool] 리스트 병합 중 예외 발생 (field: {}): {}", field.getName(), e.getMessage());
}
}
}
current = current.getSuperclass();
}
}
}

View File

@@ -0,0 +1,180 @@
package io.shinhanlife.dat.lib.util;
import jakarta.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @package io.shinhanlife.dat.lib.util
* @className CommonCodeResolver
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Slf4j
@Component
public class CommonCodeResolver {
private static final String DEFAULT_RESOURCE_PATH = "codes.csv";
// Group -> (Code -> Name)
private static final Map<String, Map<String, String>> CODE_TO_NAME_MAP = new ConcurrentHashMap<>();
// Group -> (Normalized Name or Code -> Code)
private static final Map<String, Map<String, String>> NAME_TO_CODE_MAP = new ConcurrentHashMap<>();
static {
loadCodes(DEFAULT_RESOURCE_PATH);
}
@PostConstruct
public void init() {
loadCodes(DEFAULT_RESOURCE_PATH);
}
public static synchronized void reload() {
loadCodes(DEFAULT_RESOURCE_PATH);
}
public static synchronized void loadCodes(String resourcePath) {
ClassLoader classLoader = CommonCodeResolver.class.getClassLoader();
try (InputStream is = classLoader.getResourceAsStream(resourcePath)) {
if (is == null) {
log.warn("[CommonCodeResolver] 리소스 파일을 찾을 수 없습니다: {}", resourcePath);
return;
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
CODE_TO_NAME_MAP.clear();
NAME_TO_CODE_MAP.clear();
String line;
boolean firstLine = true;
while ((line = reader.readLine()) != null) {
if (firstLine) {
firstLine = false;
if (line.startsWith("\uFEFF")) {
line = line.substring(1);
}
if (line.contains("그룹") || line.contains("group")) {
continue;
}
}
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
String[] parts = line.split(",", -1);
if (parts.length < 3) {
continue;
}
String group = parts[0].trim().toUpperCase(Locale.ROOT);
String code = parts[1].trim();
String name = parts[2].trim();
if (group.isEmpty() || code.isEmpty()) {
continue;
}
Map<String, String> codeToName = CODE_TO_NAME_MAP.computeIfAbsent(group, k -> new ConcurrentHashMap<>());
Map<String, String> nameToCode = NAME_TO_CODE_MAP.computeIfAbsent(group, k -> new ConcurrentHashMap<>());
codeToName.put(code, name);
nameToCode.put(code, code);
nameToCode.put(name, code);
nameToCode.put(name.toLowerCase(Locale.ROOT), code);
nameToCode.put(name.replace(" ", ""), code);
}
log.info("[CommonCodeResolver] 공통코드 로드 완료: {}개 그룹", CODE_TO_NAME_MAP.size());
}
} catch (Exception e) {
log.error("[CommonCodeResolver] 공통코드 로드 중 오류 발생: {}", e.getMessage(), e);
}
}
/**
* 입력값(명칭 또는 코드)을 처리계 표준 코드값으로 변환합니다.
*
* @param group 코드 그룹명 (예: COMPANY, STATUS)
* @param input 사용자가 입력한 명칭("삼성전자") 또는 코드("005930")
* @return 매핑된 코드값(예: "005930"). 매핑되지 않으면 원본 입력값 반환.
*/
public static String resolve(String group, String input) {
return resolve(group, input, input);
}
/**
* 입력값(명칭 또는 코드)을 처리계 표준 코드값으로 변환합니다.
*
* @param group 코드 그룹명
* @param input 입력값
* @param defaultValue 매핑 실패 시 반환할 기본값
* @return 매핑된 코드값 또는 defaultValue
*/
public static String resolve(String group, String input, String defaultValue) {
if (input == null || input.isBlank()) {
return defaultValue;
}
if (group == null) {
return input.trim();
}
String normalizedGroup = group.trim().toUpperCase(Locale.ROOT);
Map<String, String> nameToCode = NAME_TO_CODE_MAP.get(normalizedGroup);
if (nameToCode == null) {
return defaultValue;
}
String trimmed = input.trim();
String matched = nameToCode.get(trimmed);
if (matched != null) {
return matched;
}
matched = nameToCode.get(trimmed.toLowerCase(Locale.ROOT));
if (matched != null) {
return matched;
}
matched = nameToCode.get(trimmed.replace(" ", ""));
if (matched != null) {
return matched;
}
return defaultValue;
}
/**
* 코드값에 해당하는 표준 명칭을 반환합니다. (응답 변환 시 사용)
*/
public static String getName(String group, String code) {
if (group == null || code == null) {
return code;
}
String normalizedGroup = group.trim().toUpperCase(Locale.ROOT);
Map<String, String> codeToName = CODE_TO_NAME_MAP.get(normalizedGroup);
if (codeToName != null) {
return codeToName.getOrDefault(code.trim(), code);
}
return code;
}
/**
* 특정 그룹의 전체 코드-명칭 맵을 반환합니다.
*/
public static Map<String, String> getCodeMap(String group) {
if (group == null) {
return Collections.emptyMap();
}
Map<String, String> map = CODE_TO_NAME_MAP.get(group.trim().toUpperCase(Locale.ROOT));
return map != null ? Collections.unmodifiableMap(map) : Collections.emptyMap();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
그룹,코드,명칭
COMPANY,005930,삼성전자
COMPANY,035420,NAVER
COMPANY,035720,카카오
COMPANY,000660,SK하이닉스
COMPANY,055550,신한지주
COMPANY,086790,하나금융지주
COMPANY,105560,KB금융
COMPANY,316140,우리금융지주
COMPANY,000810,삼성화재
COMPANY,088350,한화생명
COMPANY,005830,DB손해보험
COMPANY,000030,우리은행
STATUS,01,정상
STATUS,02,비정상
STATUS,03,해지
STATUS,04,실효
1 그룹 코드 명칭
2 COMPANY 005930 삼성전자
3 COMPANY 035420 NAVER
4 COMPANY 035720 카카오
5 COMPANY 000660 SK하이닉스
6 COMPANY 055550 신한지주
7 COMPANY 086790 하나금융지주
8 COMPANY 105560 KB금융
9 COMPANY 316140 우리금융지주
10 COMPANY 000810 삼성화재
11 COMPANY 088350 한화생명
12 COMPANY 005830 DB손해보험
13 COMPANY 000030 우리은행
14 STATUS 01 정상
15 STATUS 02 비정상
16 STATUS 03 해지
17 STATUS 04 실효

View File

@@ -0,0 +1,62 @@
package io.shinhanlife.dat.lib.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
/**
* @package io.shinhanlife.dat.lib.util
* @className CommonCodeResolverTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
class CommonCodeResolverTest {
@Test
void resolvesCompanyCodeFromExactName() {
assertEquals("005930", CommonCodeResolver.resolve("COMPANY", "삼성전자"));
assertEquals("035420", CommonCodeResolver.resolve("COMPANY", "NAVER"));
assertEquals("035720", CommonCodeResolver.resolve("COMPANY", "카카오"));
assertEquals("055550", CommonCodeResolver.resolve("COMPANY", "신한지주"));
}
@Test
void resolvesCompanyCodeWhenAlreadyCode() {
assertEquals("005930", CommonCodeResolver.resolve("COMPANY", "005930"));
assertEquals("035420", CommonCodeResolver.resolve("COMPANY", "035420"));
}
@Test
void resolvesCaseInsensitiveAndSpaceIgnored() {
assertEquals("035420", CommonCodeResolver.resolve("COMPANY", "naver"));
assertEquals("005930", CommonCodeResolver.resolve("COMPANY", "삼성 전자"));
}
@Test
void resolvesStatusGroupCode() {
assertEquals("01", CommonCodeResolver.resolve("STATUS", "정상"));
assertEquals("02", CommonCodeResolver.resolve("STATUS", "비정상"));
assertEquals("03", CommonCodeResolver.resolve("STATUS", "해지"));
assertEquals("01", CommonCodeResolver.resolve("STATUS", "01"));
}
@Test
void getsNameFromCode() {
assertEquals("삼성전자", CommonCodeResolver.getName("COMPANY", "005930"));
assertEquals("정상", CommonCodeResolver.getName("STATUS", "01"));
}
@Test
void returnsOriginalWhenNotFound() {
assertEquals("알수없는회사", CommonCodeResolver.resolve("COMPANY", "알수없는회사"));
assertEquals("99", CommonCodeResolver.resolve("STATUS", "99"));
}
}

View File

@@ -0,0 +1,38 @@
package io.shinhanlife.dat.mcc.biz.pro.converter;
import io.shinhanlife.dat.lib.util.CommonCodeResolver;
import io.shinhanlife.dat.mcc.biz.pro.dto.RecruitmentFeeDisclosureDataRequest;
import io.shinhanlife.dat.mcc.biz.pro.dto.RecruitmentFeeDisclosureDataResponse;
import io.shinhanlife.dat.mcc.infra.itrf.mci.nbt.i.io.ONBTI2370_I;
import io.shinhanlife.dat.mcc.infra.itrf.mci.nbt.i.io.ONBTI2370_O;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
/**
* @package io.shinhanlife.dat.mcc.biz.pro.converter
* @className RecruitmentFeeDisclosureDataConverter
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface RecruitmentFeeDisclosureDataConverter {
@Mapping(target = "companyCode", expression = "java(toCompanyCode(request.getCompanyCode()))")
ONBTI2370_I toLegacyRequest(RecruitmentFeeDisclosureDataRequest request);
default String toCompanyCode(String company) {
return CommonCodeResolver.resolve("COMPANY", company);
}
RecruitmentFeeDisclosureDataRequest toRequest(ONBTI2370_I mciRequest);
RecruitmentFeeDisclosureDataResponse toResponse(ONBTI2370_O mciRes);
}

View File

@@ -0,0 +1,45 @@
package io.shinhanlife.dat.mcc.biz.pro.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.Pattern;
/**
* @package io.shinhanlife.dat.mcc.biz.pro.dto
* @className RecruitmentFeeDisclosureDataRequest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RecruitmentFeeDisclosureDataRequest {
@Schema(description = "조회할 회사 코드 또는 회사명 (예시: 005930, 삼성전자, NAVER, 카카오)", example = "삼성전자", requiredMode = Schema.RequiredMode.REQUIRED)
private String companyCode;
@Schema(description = "조회할 연도 (형식: ^[0-9]{4}$) (예시: 2023, 2024)", example = "2023", requiredMode = Schema.RequiredMode.REQUIRED)
@Pattern(regexp = "^[0-9]{4}$")
private String year;
@Schema(description = "조회할 분기 (1-4) (형식: ^[1-4]$) (예시: 1, 2, 3, 4)", example = "1")
@Pattern(regexp = "^[1-4]$")
private String quarter;
@Schema(description = "조회 시작일 (YYYY-MM-DD) (형식: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$) (예시: 2024-01-01, 2024-04-01)", example = "2024-01-01")
@Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$")
private String startDate;
@Schema(description = "조회 종료일 (YYYY-MM-DD) (형식: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$) (예시: 2024-03-31, 2024-06-30)", example = "2024-03-31")
@Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$")
private String endDate;
}

View File

@@ -0,0 +1,67 @@
package io.shinhanlife.dat.mcc.biz.pro.converter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import io.shinhanlife.dat.mcc.biz.pro.dto.RecruitmentFeeDisclosureDataRequest;
import io.shinhanlife.dat.mcc.infra.itrf.mci.nbt.i.io.ONBTI2370_I;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
/**
* @package io.shinhanlife.dat.mcc.biz.pro.converter
* @className RecruitmentFeeDisclosureDataConverterTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
class RecruitmentFeeDisclosureDataConverterTest {
private final RecruitmentFeeDisclosureDataConverter converter =
Mappers.getMapper(RecruitmentFeeDisclosureDataConverter.class);
@Test
void mapsCompanyNameToLegacyCompanyCode() {
RecruitmentFeeDisclosureDataRequest request = new RecruitmentFeeDisclosureDataRequest();
request.setCompanyCode("삼성전자");
request.setYear("2024");
request.setQuarter("1");
ONBTI2370_I legacyReq = converter.toLegacyRequest(request);
assertNotNull(legacyReq);
assertEquals("005930", legacyReq.getCompanyCode());
assertEquals("2024", legacyReq.getYear());
assertEquals("1", legacyReq.getQuarter());
}
@Test
void preservesCompanyCodeWhenAlreadyNumericCode() {
RecruitmentFeeDisclosureDataRequest request = new RecruitmentFeeDisclosureDataRequest();
request.setCompanyCode("035420");
request.setYear("2024");
ONBTI2370_I legacyReq = converter.toLegacyRequest(request);
assertNotNull(legacyReq);
assertEquals("035420", legacyReq.getCompanyCode());
}
@Test
void mapsNaverAndKakao() {
RecruitmentFeeDisclosureDataRequest requestNaver = new RecruitmentFeeDisclosureDataRequest();
requestNaver.setCompanyCode("NAVER");
assertEquals("035420", converter.toLegacyRequest(requestNaver).getCompanyCode());
RecruitmentFeeDisclosureDataRequest requestKakao = new RecruitmentFeeDisclosureDataRequest();
requestKakao.setCompanyCode("카카오");
assertEquals("035720", converter.toLegacyRequest(requestKakao).getCompanyCode());
}
}

7
dat-was-sal/Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
COPY dat-was-sal/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8083
ENTRYPOINT ["java", "-jar", "app.jar"]

9
dat-was-sal/build.gradle Normal file
View File

@@ -0,0 +1,9 @@
plugins {
// Spring Boot 3.5.11 version is managed by the root build.gradle.
id 'org.springframework.boot'
}
dependencies {
// Shared MCP, Glow integration, validation, logging, Lombok and MapStruct configuration.
implementation project(':dat-was-lib')
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.dat.mcc.sal;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
/**
* @package io.shinhanlife.dat.mcc.sal
* @className DatWasSalApplication
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.09.08
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.08 jade 최초생성
*
* </pre>
*/
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dat.mcc", "io.shinhanlife.dat.lib"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dat.mcc", "io.shinhanlife.dat.lib"})
@EnableCaching
public class DatWasSalApplication {
public static void main(String[] args) {
SpringApplication.run(DatWasSalApplication.class, args);
}
}

View File

@@ -0,0 +1,11 @@
server:
port: ${PORT:8083}
spring:
config:
activate:
on-profile: dev
import:
- classpath:application-core-dev.yml
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-dev.yml

View File

@@ -0,0 +1,7 @@
spring:
config:
activate:
on-profile: local
import:
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-local.yml

View File

@@ -0,0 +1,11 @@
server:
port: ${PORT:8083}
spring:
config:
activate:
on-profile: prod
import:
- classpath:application-core-prod.yml
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-prod.yml

View File

@@ -0,0 +1,11 @@
server:
port: ${PORT:8083}
spring:
config:
activate:
on-profile: test
import:
- classpath:application-core-test.yml
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-test.yml

View File

@@ -0,0 +1,21 @@
server:
port: 8083
spring:
application:
name: dat-was-sal
config:
import: optional:classpath:tool-service-manifest.yml
profiles:
active: local
logging:
level:
org.apache.kafka: ERROR
mcp:
namespace: ""
manifest:
bundle-id: dat-was-sal
name-prefix: ""
security:
api-key: ${TOOL_SERVER_API_KEY:tool-server-key}
tenant-domains:
TESTER-DEV: ALL

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/dat-was-sal.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/dat-was-sal-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<logger name="io.shinhanlife" level="DEBUG" />
</configuration>

View File

@@ -0,0 +1,22 @@
---
mcp:
manifest:
routing-functions:
- name: "route_to_dat-was-sal"
server-id: "dat-was-sal"
category-key: "sal"
product-boundary: "영업 업무 서버를 위한 제품 범위 정의입니다."
business-domain: "영업 조직 및 설계사 관리 도메인입니다."
business-outcome: "영업 요청의 효율적인 처리와 설계사 업무 지원을 보장합니다."
primary-entities: "영업 조직, 설계사, 고객"
capabilities: "요청 라우팅, 업무 할당, 상태 조회"
select-if: "요청의 주요 엔티티가 영업 조직 또는 설계사일 경우 선택합니다."
reject-if: "요청이 다른 도메인에 속하거나 유효하지 않은 엔티티를 포함할 경우 거부합니다."
confusable-servers:
- "dat-was-cus"
- "dat-was-ehr"
- "dat-was-pro"
- "dat-was-sys"
decision-policy: "가장 가까운 영업 서버를 선택하고, 부하를 고려하여 결정합니다."
description_serialization: "이 도구는 영업 요청을 처리하며, 영업 조직과 설계사 업무를 효율적으로 라우팅하고, 부\
하를 고려한 결정 정책을 적용하여 적절한 서버로 분배합니다."