diff --git a/docker-compose.yml b/docker-compose.yml index f87430a..55b9ac7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/ops/nginx/default.conf b/ops/nginx/default.conf index 56c939d..1d07355 100644 --- a/ops/nginx/default.conf +++ b/ops/nginx/default.conf @@ -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; } } - diff --git a/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataController.java b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataController.java new file mode 100644 index 0000000..d06df87 --- /dev/null +++ b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataController.java @@ -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 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 saveFxRates(@Valid @RequestBody MasterFxRatesUpdateRequest request, HttpSession session) { + authService.requireRole(session, "ADMIN"); + int savedCount = masterDataService.replaceFxRates(request); + return Map.of("ok", true, "savedCount", savedCount, "message", "환율정보가 저장되었습니다."); + } +} diff --git a/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataMapper.java b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataMapper.java index 09aafab..bf12971 100644 --- a/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataMapper.java +++ b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataMapper.java @@ -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> 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); +} diff --git a/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataService.java b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataService.java index 6da6ace..d0c3863 100644 --- a/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataService.java +++ b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterDataService.java @@ -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 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 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 rows) { + Set codes = new LinkedHashSet<>(); + for (MasterEntityRowRequest row : rows) { + if (!codes.add(row.getEntityCode())) { + throw new ApiException(HttpStatus.BAD_REQUEST, "중복된 법인코드는 저장할 수 없습니다."); + } + } + } + + private void validateDuplicateFxRateKeys(List rows) { + Set 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> entities, List> accounts, diff --git a/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterEntitiesUpdateRequest.java b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterEntitiesUpdateRequest.java new file mode 100644 index 0000000..ab13a3c --- /dev/null +++ b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterEntitiesUpdateRequest.java @@ -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 getRows() { + return rows; + } + + public void setRows(List rows) { + this.rows = rows; + } +} diff --git a/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterEntityRowRequest.java b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterEntityRowRequest.java new file mode 100644 index 0000000..b03d144 --- /dev/null +++ b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterEntityRowRequest.java @@ -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; + } +} diff --git a/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterFxRateRowRequest.java b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterFxRateRowRequest.java new file mode 100644 index 0000000..2b2f0b3 --- /dev/null +++ b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterFxRateRowRequest.java @@ -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; + } +} diff --git a/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterFxRatesUpdateRequest.java b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterFxRatesUpdateRequest.java new file mode 100644 index 0000000..9c2241d --- /dev/null +++ b/server/api/src/main/java/com/hanwha/nexacrodemo/master/MasterFxRatesUpdateRequest.java @@ -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 getRows() { + return rows; + } + + public void setRows(List rows) { + this.rows = rows; + } +}