Update docker-compose and Nginx configuration for sample data path; enhance MasterDataMapper and MasterDataService with methods for managing legal entities and FX rates, including normalization and validation logic.

This commit is contained in:
DongHeon Jang 2026-04-13 21:59:00 +09:00
parent 3400eb8067
commit bbaa6f3e0b
9 changed files with 286 additions and 4 deletions

View File

@ -92,7 +92,7 @@ services:
volumes:
- ./ops/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./client/nexacro-deploy:/usr/share/nginx/html:ro
- ./sample-data:/usr/share/nginx/html/sample-data:ro
- ./sample-data:/opt/hanwha/sample-data:ro
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/healthz || exit 1"]
interval: 10s

View File

@ -26,7 +26,7 @@ server {
}
location /sample-data/ {
alias /usr/share/nginx/html/sample-data/;
alias /opt/hanwha/sample-data/;
autoindex on;
}
@ -34,4 +34,3 @@ server {
try_files $uri $uri/ /index.html;
}
}

View File

@ -0,0 +1,37 @@
package com.hanwha.nexacrodemo.master;
import com.hanwha.nexacrodemo.auth.AuthService;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import java.util.Map;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/master")
public class MasterDataController {
private final AuthService authService;
private final MasterDataService masterDataService;
public MasterDataController(AuthService authService, MasterDataService masterDataService) {
this.authService = authService;
this.masterDataService = masterDataService;
}
@PutMapping("/entities")
public Map<String, Object> saveEntities(@Valid @RequestBody MasterEntitiesUpdateRequest request, HttpSession session) {
authService.requireRole(session, "ADMIN");
int savedCount = masterDataService.replaceEntities(request);
return Map.of("ok", true, "savedCount", savedCount, "message", "법인정보가 저장되었습니다.");
}
@PutMapping("/fx-rates")
public Map<String, Object> saveFxRates(@Valid @RequestBody MasterFxRatesUpdateRequest request, HttpSession session) {
authService.requireRole(session, "ADMIN");
int savedCount = masterDataService.replaceFxRates(request);
return Map.of("ok", true, "savedCount", savedCount, "message", "환율정보가 저장되었습니다.");
}
}

View File

@ -2,6 +2,8 @@ package com.hanwha.nexacrodemo.master;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@ -49,5 +51,40 @@ public interface MasterDataMapper {
order by username
""")
List<Map<String, Object>> findUsers();
}
@Delete("""
delete from legal_entity
""")
void deleteAllEntities();
@Insert("""
insert into legal_entity (
entity_code,
entity_name,
base_currency
) values (
#{entityCode},
#{entityName},
#{baseCurrency}
)
""")
void insertEntity(MasterEntityRowRequest row);
@Delete("""
delete from fx_rate
""")
void deleteAllFxRates();
@Insert("""
insert into fx_rate (
fiscal_period,
currency_code,
rate_to_krw
) values (
#{fiscalPeriod},
#{currencyCode},
#{rateToKrw}
)
""")
void insertFxRate(MasterFxRateRowRequest row);
}

View File

@ -2,11 +2,16 @@ package com.hanwha.nexacrodemo.master;
import com.hanwha.nexacrodemo.common.TxResponse;
import com.hanwha.nexacrodemo.common.MapKeyUtils;
import com.hanwha.nexacrodemo.common.ApiException;
import java.util.List;
import java.util.Map;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MasterDataService {
@ -48,6 +53,79 @@ public class MasterDataService {
);
}
@Transactional
public int replaceEntities(MasterEntitiesUpdateRequest request) {
List<MasterEntityRowRequest> rows = request.getRows().stream()
.map(this::normalizeEntity)
.toList();
validateDuplicateEntityCodes(rows);
masterDataMapper.deleteAllEntities();
rows.forEach(masterDataMapper::insertEntity);
return rows.size();
}
@Transactional
public int replaceFxRates(MasterFxRatesUpdateRequest request) {
List<MasterFxRateRowRequest> rows = request.getRows().stream()
.map(this::normalizeFxRate)
.toList();
validateDuplicateFxRateKeys(rows);
masterDataMapper.deleteAllFxRates();
rows.forEach(masterDataMapper::insertFxRate);
return rows.size();
}
private MasterEntityRowRequest normalizeEntity(MasterEntityRowRequest source) {
MasterEntityRowRequest row = new MasterEntityRowRequest();
row.setEntityCode(trimToNull(source.getEntityCode()));
row.setEntityName(trimToNull(source.getEntityName()));
row.setBaseCurrency(upper(trimToNull(source.getBaseCurrency())));
return row;
}
private MasterFxRateRowRequest normalizeFxRate(MasterFxRateRowRequest source) {
MasterFxRateRowRequest row = new MasterFxRateRowRequest();
row.setFiscalPeriod(trimToNull(source.getFiscalPeriod()));
row.setCurrencyCode(upper(trimToNull(source.getCurrencyCode())));
row.setRateToKrw(source.getRateToKrw());
return row;
}
private void validateDuplicateEntityCodes(List<MasterEntityRowRequest> rows) {
Set<String> codes = new LinkedHashSet<>();
for (MasterEntityRowRequest row : rows) {
if (!codes.add(row.getEntityCode())) {
throw new ApiException(HttpStatus.BAD_REQUEST, "중복된 법인코드는 저장할 수 없습니다.");
}
}
}
private void validateDuplicateFxRateKeys(List<MasterFxRateRowRequest> rows) {
Set<String> keys = new LinkedHashSet<>();
for (MasterFxRateRowRequest row : rows) {
String key = row.getFiscalPeriod() + "|" + row.getCurrencyCode();
if (!keys.add(key)) {
throw new ApiException(HttpStatus.BAD_REQUEST, "중복된 회계기간/통화 조합은 저장할 수 없습니다.");
}
}
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private String upper(String value) {
return value == null ? null : value.toUpperCase(Locale.ROOT);
}
public record ReferenceSnapshot(
List<Map<String, Object>> entities,
List<Map<String, Object>> accounts,

View File

@ -0,0 +1,20 @@
package com.hanwha.nexacrodemo.master;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
public class MasterEntitiesUpdateRequest {
@NotNull(message = "rows는 필수입니다.")
private List<@Valid MasterEntityRowRequest> rows = new ArrayList<>();
public List<MasterEntityRowRequest> getRows() {
return rows;
}
public void setRows(List<MasterEntityRowRequest> rows) {
this.rows = rows;
}
}

View File

@ -0,0 +1,44 @@
package com.hanwha.nexacrodemo.master;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
public class MasterEntityRowRequest {
@NotBlank(message = "법인코드는 필수입니다.")
@Size(max = 20, message = "법인코드는 20자 이하여야 합니다.")
private String entityCode;
@NotBlank(message = "법인명은 필수입니다.")
@Size(max = 100, message = "법인명은 100자 이하여야 합니다.")
private String entityName;
@NotBlank(message = "통화는 필수입니다.")
@Pattern(regexp = "^[A-Z]{3}$", message = "통화는 영문 대문자 3자리여야 합니다.")
private String baseCurrency;
public String getEntityCode() {
return entityCode;
}
public void setEntityCode(String entityCode) {
this.entityCode = entityCode;
}
public String getEntityName() {
return entityName;
}
public void setEntityName(String entityName) {
this.entityName = entityName;
}
public String getBaseCurrency() {
return baseCurrency;
}
public void setBaseCurrency(String baseCurrency) {
this.baseCurrency = baseCurrency;
}
}

View File

@ -0,0 +1,47 @@
package com.hanwha.nexacrodemo.master;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
public class MasterFxRateRowRequest {
@NotBlank(message = "회계기간은 필수입니다.")
@Pattern(regexp = "^\\d{4}-\\d{2}$", message = "회계기간은 YYYY-MM 형식이어야 합니다.")
private String fiscalPeriod;
@NotBlank(message = "통화는 필수입니다.")
@Size(max = 10, message = "통화 코드는 10자 이하여야 합니다.")
private String currencyCode;
@NotNull(message = "환산율은 필수입니다.")
@DecimalMin(value = "0.0", inclusive = false, message = "환산율은 0보다 커야 합니다.")
private BigDecimal rateToKrw;
public String getFiscalPeriod() {
return fiscalPeriod;
}
public void setFiscalPeriod(String fiscalPeriod) {
this.fiscalPeriod = fiscalPeriod;
}
public String getCurrencyCode() {
return currencyCode;
}
public void setCurrencyCode(String currencyCode) {
this.currencyCode = currencyCode;
}
public BigDecimal getRateToKrw() {
return rateToKrw;
}
public void setRateToKrw(BigDecimal rateToKrw) {
this.rateToKrw = rateToKrw;
}
}

View File

@ -0,0 +1,20 @@
package com.hanwha.nexacrodemo.master;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
public class MasterFxRatesUpdateRequest {
@NotNull(message = "rows는 필수입니다.")
private List<@Valid MasterFxRateRowRequest> rows = new ArrayList<>();
public List<MasterFxRateRowRequest> getRows() {
return rows;
}
public void setRows(List<MasterFxRateRowRequest> rows) {
this.rows = rows;
}
}