chore: acquire-core migration monorepo (legacy C + Spring Boot target skeleton)
Some checks failed
ci / build (push) Failing after 2s
Some checks failed
ci / build (push) Failing after 2s
TxCore/ECPG legacy 매입·정산 C 슬라이스(빌드검증) + Spring Boot 멀티모듈 골격(mvn test green) + MIGRATION.md 전환룰 + CI(boot 빌드).
This commit is contained in:
commit
ff0b0b60a2
31 changed files with 2208 additions and 0 deletions
9
.forgejo/workflows/ci.yml
Normal file
9
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
name: ci
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
git clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" repo && cd repo && git checkout "$GITHUB_SHA"
|
||||||
|
cd boot && mvn -q -B -DskipTests=false test
|
||||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# ── Spring / Maven 타겟 ──────────────────────────────────────────────
|
||||||
|
target/
|
||||||
|
*.class
|
||||||
|
*.jar
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
# ── 레거시 C 빌드 산출물 ─────────────────────────────────────────────
|
||||||
|
build/
|
||||||
|
bin/
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
core
|
||||||
|
|
||||||
|
# ECPG 생성 소스 (.pgc -> .c)
|
||||||
|
legacy/app/dbio/*.c
|
||||||
|
legacy/app/online/mg_recv_svc.c
|
||||||
|
legacy/app/online/ac_intake_svc.c
|
||||||
|
legacy/app/batch/*.c
|
||||||
|
|
||||||
|
# ── 기타 ─────────────────────────────────────────────────────────────
|
||||||
|
*.log
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.iml
|
||||||
59
MIGRATION.md
Normal file
59
MIGRATION.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# MIGRATION — acquire-core C → Spring Boot 전환 플레이북
|
||||||
|
|
||||||
|
이 저장소는 레거시 C 슬라이스(`legacy/`)를 Spring Boot 멀티모듈(`boot/`)로
|
||||||
|
이식하기 위한 **변환 플레이북**이다. 에이전트는 아래 규칙표·순서·배치 규약을
|
||||||
|
그대로 따른다.
|
||||||
|
|
||||||
|
## 대상 슬라이스
|
||||||
|
|
||||||
|
카드 **매입·정산(acquiring/settlement)** 수직 슬라이스. 레거시는 사내 표준
|
||||||
|
공통 프레임워크 **TxCore**(Tuxedo ATMI + FML/UBF 관용구의 자체 C 구현) 위에서
|
||||||
|
**ECPG**(Pro\*C 상당 임베디드 SQL)로 작성되어 있다.
|
||||||
|
|
||||||
|
## 규칙표 (레거시 → Spring)
|
||||||
|
|
||||||
|
| 레거시 C (source) | Spring Boot 타겟 (target) | 비고 |
|
||||||
|
|-------------------|---------------------------|------|
|
||||||
|
| TxCore `TX_SERVICE` / 서비스 레지스트리 (`tpsvrinit` 등록) | `@Service` / `@Component` + `ApplicationContext` 빈 조회 | in-process 디스패처 → Spring 빈 컨테이너 |
|
||||||
|
| `tx_call(SVC, buf)` (서비스 간 호출) | 내부 빈 메서드 호출, 원격이면 Feign 클라이언트 | `tpcall`/`tpreturn` 상당 |
|
||||||
|
| `TXBUF` (FML/UBF 고정 슬롯 키/값) | 요청/응답 **DTO(record)** + 컨텍스트 `Map<String,Object>` | 정형 필드는 record, 느슨한 상태만 컨텍스트 |
|
||||||
|
| `tx_begin` / `tx_commit` / `tx_abort` (XA 스텁) | `@Transactional` (`PlatformTransactionManager`) | 선언적 트랜잭션 |
|
||||||
|
| `tx_log` | SLF4J / Logback | |
|
||||||
|
| `*.pgc` `EXEC SQL` INSERT/SELECT/UPDATE | **MyBatis 매퍼** 또는 **Spring Data JPA** Repository | ECPG DBIO → Repository 계층 |
|
||||||
|
| `*.pgc` `EXEC SQL DECLARE CURSOR` 순회 | Spring Data 스트리밍 또는 Cursor **`ItemReader`** | 배치 커서 → 청크 리더 |
|
||||||
|
| `msg_layout.h` 고정길이(positional) 전문 + `util_msg.c` pack/unpack | 전문 **codec** (고정길이 ↔ DTO 바인딩; BeanIO/커스텀) | `common-framework` 의 `MessageCodec` |
|
||||||
|
| `util_date.c` / `util_amount.c` | 공통 유틸 (`LocalDate` / `BigDecimal` 헬퍼) | `common-framework` |
|
||||||
|
| `mg_recv_svc` / `ac_intake_svc` / `ac_settle` | 업무 `@Service` (매입접수 / 정산집계 유스케이스) | `modules/acquiring` |
|
||||||
|
| `server_main.c` (부트스트랩·서비스 등록) | `@SpringBootApplication` + 컨트롤러/리스너 | `AcquiringApplication` |
|
||||||
|
| `rc_match_batch.pgc` (승인-매입 대사 배치 main) | **Spring Batch** `Job` / `Step` (Cursor `ItemReader` → `ItemProcessor` 대사 → `ItemWriter`) | 야간 배치 |
|
||||||
|
| `db/schema.sql` (purchase/approval/settlement DDL) | **Flyway** (또는 Liquibase) 마이그레이션 | `src/main/resources/db/migration` |
|
||||||
|
|
||||||
|
## 타겟 패키지 규약
|
||||||
|
|
||||||
|
- 루트 패키지: **`com.klaro.acquirecore.*`**
|
||||||
|
- 공통 프레임워크: `com.klaro.acquirecore.framework.*` → `boot/common-framework/`
|
||||||
|
- 업무 모듈(매입): `com.klaro.acquirecore.acquiring.*` → `boot/modules/acquiring/`
|
||||||
|
- 이후 정산 등 모듈 추가 시 `com.klaro.acquirecore.<module>` + `boot/modules/<module>/`
|
||||||
|
|
||||||
|
## 2단계 의존 순서 (반드시 준수)
|
||||||
|
|
||||||
|
1. **1단계 — 공통 프레임워크 우선.** TxCore 대체(`common-framework`)가 먼저
|
||||||
|
착지해야 한다. 업무 모듈이 의존하는 `TxContext` / `MessageCodec` / 트랜잭션·로깅
|
||||||
|
규약이 여기 있다. 프레임워크 없이 업무 모듈만 이식하면 빌드가 깨진다.
|
||||||
|
2. **2단계 — 업무 모듈.** `modules/acquiring` 은 `common-framework` 에 의존한다
|
||||||
|
(`pom.xml` `<dependency>`). 매입접수 → 정산집계 → 대사 배치 순으로 유스케이스를
|
||||||
|
채운다.
|
||||||
|
|
||||||
|
## 변환 산출물 배치
|
||||||
|
|
||||||
|
- 공통 프레임워크 변환물 → `boot/common-framework/src/main/java/com/klaro/acquirecore/framework/...`
|
||||||
|
- 업무 모듈 변환물 → `boot/modules/<module>/src/main/java/com/klaro/acquirecore/<module>/...`
|
||||||
|
- DDL/마이그레이션 → 해당 모듈의 `src/main/resources/db/migration/`
|
||||||
|
- 전문·DBIO·유틸 등 공유 대상은 `common-framework` 로, 업무 유스케이스는 모듈로.
|
||||||
|
|
||||||
|
## 원칙
|
||||||
|
|
||||||
|
- `legacy/` 는 **읽기 전용 소스**다. 에이전트는 수정하지 않는다.
|
||||||
|
- 각 변환은 컴파일 + `mvn test` 초록을 유지한 채 증분으로 진행한다.
|
||||||
|
- 스켈레톤 단계에서는 데이터소스를 배선하지 않는다(스모크 테스트는 Postgres 불필요).
|
||||||
|
DBIO 이식 시점에 Flyway + datasource 를 추가한다.
|
||||||
48
README.md
Normal file
48
README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# acquire-core-migration
|
||||||
|
|
||||||
|
카드 **매입·정산** 레거시 C 슬라이스를 **Spring Boot** 멀티모듈로 전환하기 위한
|
||||||
|
**마이그레이션 모노레포**(테스트 픽스처). 소스(레거시)와 타겟(Spring 스켈레톤)을
|
||||||
|
한 저장소에 두고, `MIGRATION.md` 의 규칙표를 따라 에이전트가 증분 이식한다.
|
||||||
|
|
||||||
|
## 레이아웃
|
||||||
|
|
||||||
|
```
|
||||||
|
acquire-core-migration/
|
||||||
|
├── legacy/ # SOURCE — 빌드되는 레거시 C 슬라이스 (TxCore 프레임워크 + 매입/정산 앱). 읽기 전용.
|
||||||
|
├── boot/ # TARGET — Spring Boot 멀티모듈 스켈레톤 (Java 17, Spring Boot 3.3.x)
|
||||||
|
│ ├── pom.xml # 부모 (packaging pom)
|
||||||
|
│ ├── common-framework/ # 모듈 1 — TxCore 의 Spring 대체 스타터 (라이브러리 jar)
|
||||||
|
│ └── modules/acquiring/ # 모듈 2 — 매입 업무 (@SpringBootApplication), common-framework 의존
|
||||||
|
├── MIGRATION.md # 전환 플레이북 (레거시→Spring 규칙표 + 배치 규약)
|
||||||
|
├── .forgejo/workflows/ci.yml # CI — Spring 타겟 빌드/테스트
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
## 두 단계 전환 계획
|
||||||
|
|
||||||
|
1. **공통 프레임워크 우선.** TxCore(`legacy/framework/txcore/`) → `boot/common-framework/`.
|
||||||
|
서비스 디스패처·TXBUF·트랜잭션·로깅·전문 코덱 규약을 먼저 착지시킨다.
|
||||||
|
2. **업무 모듈.** 매입/정산(`legacy/app/`) → `boot/modules/acquiring/`.
|
||||||
|
`common-framework` 에 의존하며 매입접수 → 정산집계 → 대사 배치 순으로 채운다.
|
||||||
|
|
||||||
|
자세한 매핑은 [`MIGRATION.md`](./MIGRATION.md).
|
||||||
|
|
||||||
|
## 빌드
|
||||||
|
|
||||||
|
### 타겟 (Spring Boot 스켈레톤)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm -v "$PWD":/src -w /src/boot maven:3.9-eclipse-temurin-17 \
|
||||||
|
bash -lc "mvn -q -B -DskipTests=false test"
|
||||||
|
```
|
||||||
|
|
||||||
|
빈 스켈레톤이 컴파일되고 스모크 테스트가 초록이면 성공. DB 불필요.
|
||||||
|
|
||||||
|
### 소스 (레거시 C, 참고용)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm -v "$PWD/legacy":/src -w /src debian:12 bash -lc \
|
||||||
|
"apt-get update -qq && apt-get install -y -qq build-essential libecpg-dev libpq-dev postgresql-server-dev-all >/dev/null 2>&1; make clean; make"
|
||||||
|
```
|
||||||
|
|
||||||
|
레거시 슬라이스의 상세는 [`legacy/README.md`](./legacy/README.md).
|
||||||
40
boot/common-framework/pom.xml
Normal file
40
boot/common-framework/pom.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.klaro.acquirecore</groupId>
|
||||||
|
<artifactId>acquire-core-boot</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>common-framework</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>common-framework</name>
|
||||||
|
<description>TxCore 의 Spring 대체 공통 프레임워크 스타터 (라이브러리 jar)</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- 라이브러리이므로 web/실행 스타터 없이 코어 스타터만 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<!-- 라이브러리 모듈은 실행 가능 jar 재패키징을 하지 않는다 -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<skip>true</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.klaro.acquirecore.framework;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 레거시 {@code msg_layout.h} 고정길이 전문(電文) 코덱의 Spring 빈 자리표시자.
|
||||||
|
*
|
||||||
|
* <p>실 전환에서는 고정길이(positional) 바이트 레이아웃 ↔ DTO 바인딩을 담당한다
|
||||||
|
* (BeanIO 또는 커스텀 코덱). 지금은 스켈레톤이 빈으로 뜨는지만 증명한다.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class MessageCodec {
|
||||||
|
|
||||||
|
/** 고정길이 필드를 우측을 공백으로 채워 정규화한다 (자리표시자 구현). */
|
||||||
|
public String padRight(String value, int width) {
|
||||||
|
if (value == null) {
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
if (value.length() >= width) {
|
||||||
|
return value.substring(0, width);
|
||||||
|
}
|
||||||
|
return String.format("%-" + width + "s", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.klaro.acquirecore.framework;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 레거시 TxCore {@code TXBUF} (고정 슬롯 키/값 버퍼) 의 Spring 대체 자리표시자.
|
||||||
|
*
|
||||||
|
* <p>실 전환에서는 요청/응답 DTO(record) 가 정형 필드를 담고, 이 컨텍스트는
|
||||||
|
* 서비스 체인({@code tx_call}) 간 전달되는 느슨한 키/값 상태만 보관한다.
|
||||||
|
*/
|
||||||
|
public final class TxContext {
|
||||||
|
|
||||||
|
private final Map<String, Object> slots = new HashMap<>();
|
||||||
|
|
||||||
|
/** TXBUF 필드 설정 (레거시 {@code Fchg} 상당). */
|
||||||
|
public TxContext put(String key, Object value) {
|
||||||
|
slots.put(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TXBUF 필드 조회 (레거시 {@code Fget} 상당). */
|
||||||
|
public Object get(String key) {
|
||||||
|
return slots.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 적재된 슬롯 개수. */
|
||||||
|
public int size() {
|
||||||
|
return slots.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* TxCore 공통 프레임워크의 Spring 대체 계층.
|
||||||
|
*
|
||||||
|
* <p>레거시 {@code framework/txcore/} (in-process 서비스 디스패처 + 고정 슬롯 TXBUF +
|
||||||
|
* XA 스텁) 를 Spring 관용구로 이식한 자리표시자 스타터다. 실제 전환 시 서비스
|
||||||
|
* 레지스트리는 {@code ApplicationContext} 빈 조회로, {@code tx_call} 은 내부
|
||||||
|
* 빈 호출/Feign 으로, {@code tx_begin/commit} 은 {@code @Transactional} 로 대체된다.
|
||||||
|
*
|
||||||
|
* @see com.klaro.acquirecore.framework.TxContext
|
||||||
|
* @see com.klaro.acquirecore.framework.MessageCodec
|
||||||
|
*/
|
||||||
|
package com.klaro.acquirecore.framework;
|
||||||
47
boot/modules/acquiring/pom.xml
Normal file
47
boot/modules/acquiring/pom.xml
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.klaro.acquirecore</groupId>
|
||||||
|
<artifactId>acquire-core-boot</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<relativePath>../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>acquiring</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>acquiring</name>
|
||||||
|
<description>매입(acquiring) 업무 모듈 — common-framework 에 의존하는 Spring Boot 앱</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- 2단계 의존: 공통 프레임워크가 먼저 배치되어야 한다 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.klaro.acquirecore</groupId>
|
||||||
|
<artifactId>common-framework</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.klaro.acquirecore.acquiring;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매입(acquiring) 업무 모듈 부트스트랩.
|
||||||
|
*
|
||||||
|
* <p>레거시 {@code app/online/server_main.c} (tpsvrinit 서비스 등록) 의 Spring 대체.
|
||||||
|
* common-framework(TxCore 대체) 패키지를 컴포넌트 스캔에 포함해 {@code MessageCodec}
|
||||||
|
* 등 프레임워크 빈을 함께 올린다.
|
||||||
|
*/
|
||||||
|
@SpringBootApplication(scanBasePackages = {
|
||||||
|
"com.klaro.acquirecore.acquiring",
|
||||||
|
"com.klaro.acquirecore.framework"
|
||||||
|
})
|
||||||
|
public class AcquiringApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(AcquiringApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: acquiring
|
||||||
|
# 스켈레톤 단계에서는 데이터소스를 배선하지 않는다(스모크 테스트가 Postgres 불필요).
|
||||||
|
# 매입 DBIO(purchase_dbio.pgc → MyBatis/Spring Data) 이식 시 datasource 를 추가한다.
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.klaro.acquirecore: INFO
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.klaro.acquirecore.acquiring;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스켈레톤 스모크 테스트.
|
||||||
|
*
|
||||||
|
* <p>DB/컨텍스트 없이 순수 JUnit 으로 빌드 파이프라인(컴파일 + `mvn test`)이
|
||||||
|
* 초록인지만 증명한다. common-framework 이식이 진행되면 실제 유스케이스
|
||||||
|
* 테스트로 대체된다.
|
||||||
|
*/
|
||||||
|
class SmokeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void skeletonBuilds() {
|
||||||
|
assertTrue(true, "Spring Boot 매입 모듈 스켈레톤이 빌드된다");
|
||||||
|
}
|
||||||
|
}
|
||||||
43
boot/pom.xml
Normal file
43
boot/pom.xml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.3.5</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.klaro.acquirecore</groupId>
|
||||||
|
<artifactId>acquire-core-boot</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<name>acquire-core-boot</name>
|
||||||
|
<description>Spring Boot 전환 타겟: TxCore 공통 프레임워크 + 매입 업무 모듈</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>common-framework</module>
|
||||||
|
<module>modules/acquiring</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<!-- 내부 모듈: common-framework 를 업무 모듈이 참조 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.klaro.acquirecore</groupId>
|
||||||
|
<artifactId>common-framework</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
</project>
|
||||||
18
legacy/.gitignore
vendored
Normal file
18
legacy/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# 빌드 산출물
|
||||||
|
build/
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# ECPG 생성 소스 (.pgc -> .c)
|
||||||
|
app/dbio/*.c
|
||||||
|
app/online/mg_recv_svc.c
|
||||||
|
app/online/ac_intake_svc.c
|
||||||
|
app/batch/*.c
|
||||||
|
|
||||||
|
# 오브젝트/라이브러리
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# 기타
|
||||||
|
*.log
|
||||||
|
core
|
||||||
120
legacy/Makefile
Normal file
120
legacy/Makefile
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# ==========================================================================
|
||||||
|
# acquire-core / TxCore Makefile
|
||||||
|
#
|
||||||
|
# .pgc --ecpg--> .c --gcc--> .o --ld--> 실행파일
|
||||||
|
#
|
||||||
|
# 산출물:
|
||||||
|
# build/libtxcore.a TxCore 공통 프레임워크 정적 라이브러리
|
||||||
|
# bin/acquire-core-server 온라인 서비스 등록 데모 (링크 검증)
|
||||||
|
# bin/rc_match_batch 승인-매입 대사 배치
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
CC := gcc
|
||||||
|
ECPG := ecpg
|
||||||
|
AR := ar
|
||||||
|
PG_INCDIR := $(shell pg_config --includedir 2>/dev/null)
|
||||||
|
|
||||||
|
# 헤더 검색 경로 (프레임워크 + 업무 + PostgreSQL/ecpglib/sqlca)
|
||||||
|
INCLUDES := -Iframework/txcore/include \
|
||||||
|
-Iframework/txcore/dbio \
|
||||||
|
-Iapp/include \
|
||||||
|
-I$(PG_INCDIR)
|
||||||
|
|
||||||
|
# 고정길이 필드 strncpy/snprintf 는 의도된 관용구이므로 해당 경고만 억제
|
||||||
|
CFLAGS := -g -O2 -Wall -Wextra -Wno-unused-parameter \
|
||||||
|
-Wno-stringop-truncation -Wno-format-truncation $(INCLUDES)
|
||||||
|
# ecpg 는 자체 전처리기이므로 -I 로 EXEC SQL INCLUDE 및 헤더 경로를 제공
|
||||||
|
ECPGFLAGS := -Iframework/txcore/include -Iframework/txcore/dbio -Iapp/include
|
||||||
|
LDLIBS := -lecpg -lpq
|
||||||
|
|
||||||
|
BUILD := build
|
||||||
|
BIN := bin
|
||||||
|
|
||||||
|
# ---- 소스 목록 ------------------------------------------------------------
|
||||||
|
TXCORE_SRC := framework/txcore/src/txcore.c
|
||||||
|
COMMON_SRC := app/common/util_date.c \
|
||||||
|
app/common/util_amount.c \
|
||||||
|
app/common/util_msg.c
|
||||||
|
|
||||||
|
# ECPG 전처리 대상
|
||||||
|
DBIO_PGC := app/dbio/purchase_dbio.pgc
|
||||||
|
SVC_PGC := app/online/mg_recv_svc.pgc \
|
||||||
|
app/online/ac_intake_svc.pgc
|
||||||
|
BATCH_PGC := app/batch/rc_match_batch.pgc
|
||||||
|
|
||||||
|
# .pgc -> 생성 .c
|
||||||
|
DBIO_GEN := $(DBIO_PGC:.pgc=.c)
|
||||||
|
SVC_GEN := $(SVC_PGC:.pgc=.c)
|
||||||
|
BATCH_GEN := $(BATCH_PGC:.pgc=.c)
|
||||||
|
|
||||||
|
# ---- 오브젝트 -------------------------------------------------------------
|
||||||
|
TXCORE_OBJ := $(BUILD)/txcore.o
|
||||||
|
COMMON_OBJ := $(patsubst app/common/%.c,$(BUILD)/%.o,$(COMMON_SRC))
|
||||||
|
DBIO_OBJ := $(BUILD)/purchase_dbio.o
|
||||||
|
SVC_OBJ := $(BUILD)/mg_recv_svc.o $(BUILD)/ac_intake_svc.o
|
||||||
|
SERVER_OBJ := $(BUILD)/server_main.o
|
||||||
|
BATCH_OBJ := $(BUILD)/rc_match_batch.o
|
||||||
|
|
||||||
|
LIBTXCORE := $(BUILD)/libtxcore.a
|
||||||
|
|
||||||
|
SERVER_BIN := $(BIN)/acquire-core-server
|
||||||
|
BATCH_BIN := $(BIN)/rc_match_batch
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
.PHONY: all clean dirs gen
|
||||||
|
all: dirs $(LIBTXCORE) $(SERVER_BIN) $(BATCH_BIN)
|
||||||
|
@echo "==> 빌드 완료"
|
||||||
|
@ls -la $(LIBTXCORE) $(SERVER_BIN) $(BATCH_BIN)
|
||||||
|
|
||||||
|
dirs:
|
||||||
|
@mkdir -p $(BUILD) $(BIN)
|
||||||
|
|
||||||
|
# ---- ecpg: .pgc -> .c -----------------------------------------------------
|
||||||
|
%.c: %.pgc
|
||||||
|
$(ECPG) $(ECPGFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
gen: $(DBIO_GEN) $(SVC_GEN) $(BATCH_GEN)
|
||||||
|
|
||||||
|
# ---- TxCore 정적 라이브러리 ----------------------------------------------
|
||||||
|
$(TXCORE_OBJ): $(TXCORE_SRC)
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
$(LIBTXCORE): $(TXCORE_OBJ)
|
||||||
|
$(AR) rcs $@ $^
|
||||||
|
|
||||||
|
# ---- 공통 유틸 오브젝트 ---------------------------------------------------
|
||||||
|
$(BUILD)/%.o: app/common/%.c
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
# ---- ECPG 생성 .c 컴파일 --------------------------------------------------
|
||||||
|
$(DBIO_OBJ): $(DBIO_GEN)
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
$(BUILD)/mg_recv_svc.o: app/online/mg_recv_svc.c
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
$(BUILD)/ac_intake_svc.o: app/online/ac_intake_svc.c
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
$(BATCH_OBJ): $(BATCH_GEN)
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
# ---- server_main (plain C) ------------------------------------------------
|
||||||
|
$(SERVER_OBJ): app/online/server_main.c
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
# ---- 링크 -----------------------------------------------------------------
|
||||||
|
# 온라인 서버: 서비스들 + 공통 + DBIO + TxCore
|
||||||
|
$(SERVER_BIN): $(SERVER_OBJ) $(SVC_OBJ) $(DBIO_OBJ) $(COMMON_OBJ) $(LIBTXCORE)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $(SERVER_OBJ) $(SVC_OBJ) $(DBIO_OBJ) $(COMMON_OBJ) \
|
||||||
|
-L$(BUILD) -ltxcore $(LDLIBS)
|
||||||
|
|
||||||
|
# 배치: 배치 + DBIO + 공통 + TxCore
|
||||||
|
$(BATCH_BIN): $(BATCH_OBJ) $(DBIO_OBJ) $(COMMON_OBJ) $(LIBTXCORE)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $(BATCH_OBJ) $(DBIO_OBJ) $(COMMON_OBJ) \
|
||||||
|
-L$(BUILD) -ltxcore $(LDLIBS)
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD) $(BIN)
|
||||||
|
rm -f $(DBIO_GEN) $(SVC_GEN) $(BATCH_GEN)
|
||||||
82
legacy/README.md
Normal file
82
legacy/README.md
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# acquire-core (레거시 C 매입·정산 슬라이스)
|
||||||
|
|
||||||
|
카드 **매입·정산(acquiring/settlement)** 업무의 레거시 C 수직 슬라이스.
|
||||||
|
Tuxedo/ProFrame 계열 TP 모니터 위에서 Pro\*C(임베디드 SQL) 로 작성되던
|
||||||
|
전형적인 국내 카드사 시스템의 구조를 재현하되, **외부 TP 미들웨어 설치 없이
|
||||||
|
gcc + ecpg 만으로 빌드/링크**되도록 자체 프레임워크 계층(TxCore)을 포함한다.
|
||||||
|
|
||||||
|
## 스택
|
||||||
|
|
||||||
|
- **TxCore** — 사내 표준 공통 프레임워크. Tuxedo ATMI(`tpcall`/`tpreturn`/
|
||||||
|
`tpbegin`)와 FML/UBF(`Fchg`/`Fget`) 관용구를 얇게 감싼 자체 C 구현.
|
||||||
|
in-process 서비스 디스패치 + 고정 슬롯 키/값 버퍼로 동작하므로 Enduro/X 등
|
||||||
|
실 TP 모니터 없이 단독 링크된다. (`framework/txcore/`)
|
||||||
|
- **ECPG** — PostgreSQL 의 Pro\*C 상당 임베디드 SQL 전처리기. 업무 DBIO/서비스/
|
||||||
|
배치는 `.pgc` 에 `EXEC SQL` 로 SQL 을 임베드한다. 빌드 시 `ecpg` 가 `.c` 로
|
||||||
|
전처리하고 `libecpg`/`libpq` 에 링크된다.
|
||||||
|
- **PostgreSQL** — 매입/승인/정산 원장(`db/schema.sql`). 컴파일에는 불필요하며
|
||||||
|
실행 시 적용한다.
|
||||||
|
|
||||||
|
## 모듈 맵
|
||||||
|
|
||||||
|
| 경로 | 역할 |
|
||||||
|
|------|------|
|
||||||
|
| `framework/txcore/include/txcore.h` | 공통 프레임워크 API (서비스/버퍼/트랜잭션/로그) |
|
||||||
|
| `framework/txcore/src/txcore.c` | in-process 디스패처 + 버퍼 + XA 스텁 구현 |
|
||||||
|
| `framework/txcore/dbio/txcore_dbio.h` | DBIO 결과판정/로깅 규약 (SQLCODE 매핑) |
|
||||||
|
| `app/include/msg_layout.h` | 고정길이 매입 전문(電文) 레이아웃 (100바이트 positional) |
|
||||||
|
| `app/include/acq_util.h` | 일자/금액/전문 유틸 선언 |
|
||||||
|
| `app/include/purchase_dbio.h` | 매입/정산 DBIO 인터페이스 |
|
||||||
|
| `app/common/util_date.c` | 일자 검증·요일·다음 영업일 |
|
||||||
|
| `app/common/util_amount.c` | 금액 파싱/zero-pad/원화 포맷/범위검증 |
|
||||||
|
| `app/common/util_msg.c` | 전문 pack/unpack·필드 복사·필수항목 검증 |
|
||||||
|
| `app/dbio/purchase_dbio.pgc` | ECPG DBIO: INSERT/SELECT/UPDATE/upsert + 접속 |
|
||||||
|
| `app/online/mg_recv_svc.pgc` | 전문수신 서비스(`MG_RECV`): 언팩 → TXBUF 적재 |
|
||||||
|
| `app/online/ac_intake_svc.pgc` | 매입접수 서비스(`AC_INTAKE`)+정산집계(`AC_SETTLE`): 검증→INSERT→`tx_call` |
|
||||||
|
| `app/online/server_main.c` | 온라인 서버 부트스트랩(서비스 등록, tpsvrinit 상당) |
|
||||||
|
| `app/batch/rc_match_batch.pgc` | 승인-매입 대사 배치(`RC_MATCH`): 커서 순회 + 업무규칙 + 상태갱신 |
|
||||||
|
| `db/schema.sql` | purchase/approval/settlement 테이블 DDL |
|
||||||
|
| `Makefile` | `.pgc`→`ecpg`→`.c`→`gcc`→링크 파이프라인 |
|
||||||
|
|
||||||
|
## 빌드
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm -v "$PWD":/src -w /src debian:12 bash -lc \
|
||||||
|
"apt-get update -qq && apt-get install -y -qq build-essential libecpg-dev libpq-dev postgresql-server-dev-all >/dev/null 2>&1; make clean; make"
|
||||||
|
```
|
||||||
|
|
||||||
|
산출물:
|
||||||
|
- `build/libtxcore.a` — TxCore 정적 라이브러리
|
||||||
|
- `bin/acquire-core-server` — 온라인 서비스 등록 데모 (DB 없이 실행 가능)
|
||||||
|
- `bin/rc_match_batch` — 대사 배치 (`rc_match_batch <YYYYMMDD> [db@host]`)
|
||||||
|
|
||||||
|
`libecpg-dev` 가 `ecpg` + `ecpglib` + `sqlca.h` 를, `libpq-dev` 가 `libpq` 를 제공한다.
|
||||||
|
ECPG 생성 `.c` 는 `-I $(pg_config --includedir)` 로 `sqlca.h`/`ecpglib.h` 를 찾는다.
|
||||||
|
|
||||||
|
## 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
대외 전문 → [MG_RECV] 언팩·검증 → TXBUF
|
||||||
|
→ [AC_INTAKE] 검증 → tx_begin → purchase INSERT
|
||||||
|
→ tx_call[AC_SETTLE] settlement upsert → tx_commit
|
||||||
|
(야간) [RC_MATCH] purchase(status='R') 커서 순회 → approval 대사 → status 갱신(M/U)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spring Boot 전환 타겟
|
||||||
|
|
||||||
|
이 슬라이스는 **공통 프레임워크 + 업무 모듈** 2계층으로 그대로 이식된다.
|
||||||
|
|
||||||
|
| 레거시 C | Spring Boot 타겟 |
|
||||||
|
|----------|------------------|
|
||||||
|
| `txcore.h`/`txcore.c` (tpcall/tpreturn/디스패치) | 공통 프레임워크 스타터 (요청 디스패치 = `@Service`/`@Component` + 내부 호출; 서비스 레지스트리 = Spring `ApplicationContext` 빈 조회) |
|
||||||
|
| `TXBUF` 키/값 버퍼 | 요청/응답 DTO (record) + `Map<String,Object>` 컨텍스트 |
|
||||||
|
| `tx_begin/commit/abort` (XA 스텁) | `@Transactional` (Spring `PlatformTransactionManager`) |
|
||||||
|
| `tx_log` | SLF4J/Logback |
|
||||||
|
| `msg_layout.h` 고정길이 전문 + `util_msg.c` | 전문 매핑 라이브러리 (고정길이 ↔ DTO 바인딩, 예: BeanIO/커스텀 코덱) |
|
||||||
|
| `util_date.c`/`util_amount.c` | 공통 유틸 (`common` 모듈, `LocalDate`/`BigDecimal` 헬퍼) |
|
||||||
|
| `purchase_dbio.pgc` (EXEC SQL) | Repository 계층 (MyBatis 매퍼 XML 또는 Spring Data JPA) |
|
||||||
|
| `mg_recv_svc` / `ac_intake_svc` / `ac_settle` | 업무 `@Service` (매입접수/정산집계 유스케이스) |
|
||||||
|
| `server_main.c` (tpsvrinit 등록) | `@SpringBootApplication` 부트스트랩 + 컨트롤러/리스너 |
|
||||||
|
| `rc_match_batch.pgc` (커서 배치) | Spring Batch `Job`/`Step` (Cursor `ItemReader` → `ItemProcessor` 대사 → `ItemWriter`) |
|
||||||
|
| `db/schema.sql` | Flyway/Liquibase 마이그레이션 |
|
||||||
|
```
|
||||||
147
legacy/app/batch/rc_match_batch.pgc
Normal file
147
legacy/app/batch/rc_match_batch.pgc
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
/*
|
||||||
|
* rc_match_batch.pgc - 승인-매입 대사(對査) 배치 (RC_MATCH)
|
||||||
|
*
|
||||||
|
* 지정 영업일의 접수(status='R') 매입 건을 커서로 순회하며 승인원장과
|
||||||
|
* 대사한다. 금액이 일치하면 대사완료('M'), 아니면 불일치('U') 로 갱신하고,
|
||||||
|
* 처리 요약을 출력한다. 배치 main 엔트리를 포함한다.
|
||||||
|
*
|
||||||
|
* 실행: rc_match_batch <YYYYMMDD> [db@host]
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "txcore.h"
|
||||||
|
#include "txcore_dbio.h"
|
||||||
|
#include "acq_util.h"
|
||||||
|
#include "purchase_dbio.h"
|
||||||
|
|
||||||
|
EXEC SQL INCLUDE sqlca;
|
||||||
|
|
||||||
|
static int run_match(const char *biz_date)
|
||||||
|
{
|
||||||
|
EXEC SQL BEGIN DECLARE SECTION;
|
||||||
|
char h_biz_date[9];
|
||||||
|
char h_appr_no[10];
|
||||||
|
char h_merch_id[16];
|
||||||
|
long h_amount;
|
||||||
|
char h_new_status[2];
|
||||||
|
EXEC SQL END DECLARE SECTION;
|
||||||
|
|
||||||
|
long total = 0, matched = 0, unmatched = 0, errcnt = 0;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
strncpy(h_biz_date, biz_date, sizeof(h_biz_date) - 1);
|
||||||
|
h_biz_date[sizeof(h_biz_date) - 1] = '\0';
|
||||||
|
|
||||||
|
/* 접수상태 매입 건에 대한 커서 선언 */
|
||||||
|
EXEC SQL DECLARE cur_purchase CURSOR FOR
|
||||||
|
SELECT appr_no, merch_id, amount
|
||||||
|
FROM purchase
|
||||||
|
WHERE txn_date = :h_biz_date
|
||||||
|
AND status = 'R'
|
||||||
|
ORDER BY appr_no;
|
||||||
|
|
||||||
|
EXEC SQL OPEN cur_purchase;
|
||||||
|
if (sqlca.sqlcode != TX_SQL_OK) {
|
||||||
|
TX_DBIO_LOG_ERR("OPEN cur_purchase");
|
||||||
|
return TX_EDB;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx_begin() != TX_OK)
|
||||||
|
tx_log(TX_LOG_WARN, "[RC_MATCH] tx_begin 경고");
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
EXEC SQL FETCH cur_purchase
|
||||||
|
INTO :h_appr_no, :h_merch_id, :h_amount;
|
||||||
|
|
||||||
|
if (sqlca.sqlcode == TX_SQL_NOTFOUND) /* 100 = 커서 소진 */
|
||||||
|
break;
|
||||||
|
if (sqlca.sqlcode != TX_SQL_OK) {
|
||||||
|
TX_DBIO_LOG_ERR("FETCH cur_purchase");
|
||||||
|
errcnt++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
total++;
|
||||||
|
|
||||||
|
/* 업무규칙: 승인원장에서 승인번호+금액 일치 확인 */
|
||||||
|
rc = approval_match(h_appr_no, h_amount);
|
||||||
|
if (rc == TX_OK) {
|
||||||
|
strncpy(h_new_status, PUR_ST_MATCHED, sizeof(h_new_status));
|
||||||
|
matched++;
|
||||||
|
} else if (rc == TX_ENOENT) {
|
||||||
|
strncpy(h_new_status, PUR_ST_UNMATCH, sizeof(h_new_status));
|
||||||
|
unmatched++;
|
||||||
|
} else {
|
||||||
|
tx_log(TX_LOG_ERROR, "[RC_MATCH] 승인대사 오류 appr=%s rc=%d",
|
||||||
|
h_appr_no, rc);
|
||||||
|
errcnt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = purchase_update_status(h_appr_no, h_new_status);
|
||||||
|
if (rc != TX_OK) {
|
||||||
|
tx_log(TX_LOG_ERROR, "[RC_MATCH] 상태갱신 실패 appr=%s rc=%d",
|
||||||
|
h_appr_no, rc);
|
||||||
|
errcnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EXEC SQL CLOSE cur_purchase;
|
||||||
|
|
||||||
|
if (errcnt == 0)
|
||||||
|
tx_commit();
|
||||||
|
else
|
||||||
|
tx_abort();
|
||||||
|
|
||||||
|
tx_log(TX_LOG_INFO,
|
||||||
|
"[RC_MATCH] 요약 date=%s 대상=%ld 대사완료=%ld 불일치=%ld 오류=%ld",
|
||||||
|
biz_date, total, matched, unmatched, errcnt);
|
||||||
|
|
||||||
|
printf("========== 승인-매입 대사 배치 결과 ==========\n");
|
||||||
|
printf(" 영업일 : %s\n", biz_date);
|
||||||
|
printf(" 대상 건수 : %ld\n", total);
|
||||||
|
printf(" 대사완료(M) : %ld\n", matched);
|
||||||
|
printf(" 대사불일치(U) : %ld\n", unmatched);
|
||||||
|
printf(" 오류 건수 : %ld\n", errcnt);
|
||||||
|
printf("=============================================\n");
|
||||||
|
|
||||||
|
return (errcnt == 0) ? TX_OK : TX_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
const char *biz_date;
|
||||||
|
const char *target;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
tx_set_loglevel(TX_LOG_INFO);
|
||||||
|
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "사용법: %s <YYYYMMDD> [db@host]\n", argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
biz_date = argv[1];
|
||||||
|
target = (argc >= 3) ? argv[2] : NULL;
|
||||||
|
|
||||||
|
if (!date_is_valid(biz_date)) {
|
||||||
|
fprintf(stderr, "오류: 유효하지 않은 영업일 '%s'\n", biz_date);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx_log(TX_LOG_INFO, "[RC_MATCH] 배치 시작 date=%s", biz_date);
|
||||||
|
|
||||||
|
rc = dbio_connect(target);
|
||||||
|
if (rc != TX_OK) {
|
||||||
|
tx_log(TX_LOG_ERROR, "[RC_MATCH] DB 접속 실패 rc=%d", rc);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = run_match(biz_date);
|
||||||
|
|
||||||
|
dbio_disconnect();
|
||||||
|
|
||||||
|
tx_log(TX_LOG_INFO, "[RC_MATCH] 배치 종료 rc=%d(%s)", rc, tx_strerror(rc));
|
||||||
|
return (rc == TX_OK) ? 0 : 1;
|
||||||
|
}
|
||||||
82
legacy/app/common/util_amount.c
Normal file
82
legacy/app/common/util_amount.c
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* util_amount.c - 금액/통화 유틸리티 (plain C)
|
||||||
|
*/
|
||||||
|
#include "acq_util.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* 거래금액 상한 (원). 건당 1억원 */
|
||||||
|
#define AMOUNT_MAX 100000000L
|
||||||
|
|
||||||
|
long amount_parse(const char *field, int len)
|
||||||
|
{
|
||||||
|
long v = 0;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!field || len <= 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
char c = field[i];
|
||||||
|
if (c == ' ') /* 선행 공백 허용 */
|
||||||
|
continue;
|
||||||
|
if (!isdigit((unsigned char)c))
|
||||||
|
return -1;
|
||||||
|
v = v * 10 + (c - '0');
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
int amount_format(long amount, char *out, int len)
|
||||||
|
{
|
||||||
|
char tmp[32];
|
||||||
|
int n;
|
||||||
|
|
||||||
|
if (!out || len <= 0 || amount < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
n = snprintf(tmp, sizeof(tmp), "%0*ld", len, amount);
|
||||||
|
if (n < 0 || n > len) /* 폭 초과 = 오버플로우 */
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
memcpy(out, tmp, len); /* 널 종료 없이 고정폭 복사 */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void amount_format_won(long amount, char *out, int outlen)
|
||||||
|
{
|
||||||
|
char raw[32];
|
||||||
|
int n, i, j, digits, first;
|
||||||
|
|
||||||
|
if (!out || outlen <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
n = snprintf(raw, sizeof(raw), "%ld", amount);
|
||||||
|
if (n < 0) {
|
||||||
|
out[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
first = (raw[0] == '-') ? 1 : 0;
|
||||||
|
digits = n - first;
|
||||||
|
|
||||||
|
j = 0;
|
||||||
|
if (first && j < outlen - 1)
|
||||||
|
out[j++] = '-';
|
||||||
|
|
||||||
|
for (i = first; i < n && j < outlen - 1; i++) {
|
||||||
|
int pos = i - first; /* 0-based 자릿수 위치 */
|
||||||
|
if (pos > 0 && (digits - pos) % 3 == 0)
|
||||||
|
if (j < outlen - 1)
|
||||||
|
out[j++] = ',';
|
||||||
|
out[j++] = raw[i];
|
||||||
|
}
|
||||||
|
out[j] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
int amount_is_valid(long amount)
|
||||||
|
{
|
||||||
|
return (amount >= 1 && amount <= AMOUNT_MAX) ? 1 : 0;
|
||||||
|
}
|
||||||
135
legacy/app/common/util_date.c
Normal file
135
legacy/app/common/util_date.c
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* util_date.c - 일자/영업일 유틸리티 (plain C)
|
||||||
|
*/
|
||||||
|
#include "acq_util.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
static int is_all_digit(const char *s, int len)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
if (!isdigit((unsigned char)s[i]))
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int to_int(const char *s, int len)
|
||||||
|
{
|
||||||
|
int i, v = 0;
|
||||||
|
for (i = 0; i < len; i++)
|
||||||
|
v = v * 10 + (s[i] - '0');
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int days_in_month(int y, int m)
|
||||||
|
{
|
||||||
|
static const int d[] = { 31, 28, 31, 30, 31, 30,
|
||||||
|
31, 31, 30, 31, 30, 31 };
|
||||||
|
if (m < 1 || m > 12)
|
||||||
|
return 0;
|
||||||
|
if (m == 2) {
|
||||||
|
int leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
|
||||||
|
return leap ? 29 : 28;
|
||||||
|
}
|
||||||
|
return d[m - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
int date_is_valid(const char *yyyymmdd)
|
||||||
|
{
|
||||||
|
int y, m, d;
|
||||||
|
|
||||||
|
if (!yyyymmdd || strlen(yyyymmdd) < 8)
|
||||||
|
return 0;
|
||||||
|
if (!is_all_digit(yyyymmdd, 8))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
y = to_int(yyyymmdd, 4);
|
||||||
|
m = to_int(yyyymmdd + 4, 2);
|
||||||
|
d = to_int(yyyymmdd + 6, 2);
|
||||||
|
|
||||||
|
if (y < 1900 || y > 2999)
|
||||||
|
return 0;
|
||||||
|
if (m < 1 || m > 12)
|
||||||
|
return 0;
|
||||||
|
if (d < 1 || d > days_in_month(y, m))
|
||||||
|
return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sakamoto 알고리즘: 0=일요일 ... 6=토요일 */
|
||||||
|
int date_weekday(const char *yyyymmdd)
|
||||||
|
{
|
||||||
|
static const int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
|
||||||
|
int y, m, d;
|
||||||
|
|
||||||
|
if (!date_is_valid(yyyymmdd))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
y = to_int(yyyymmdd, 4);
|
||||||
|
m = to_int(yyyymmdd + 4, 2);
|
||||||
|
d = to_int(yyyymmdd + 6, 2);
|
||||||
|
|
||||||
|
if (m < 3)
|
||||||
|
y -= 1;
|
||||||
|
return (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
int date_is_weekend(const char *yyyymmdd)
|
||||||
|
{
|
||||||
|
int w = date_weekday(yyyymmdd);
|
||||||
|
return (w == 0 || w == 6) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* YYYYMMDD 를 하루 증가시킨다 (in==out 허용) */
|
||||||
|
static void add_one_day(const char *in, char *out)
|
||||||
|
{
|
||||||
|
int y = to_int(in, 4);
|
||||||
|
int m = to_int(in + 4, 2);
|
||||||
|
int d = to_int(in + 6, 2);
|
||||||
|
|
||||||
|
d++;
|
||||||
|
if (d > days_in_month(y, m)) {
|
||||||
|
d = 1;
|
||||||
|
m++;
|
||||||
|
if (m > 12) {
|
||||||
|
m = 1;
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snprintf(out, 9, "%04d%02d%02d", y, m, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
int date_next_business(const char *yyyymmdd, char *out)
|
||||||
|
{
|
||||||
|
char cur[9];
|
||||||
|
int guard = 0;
|
||||||
|
|
||||||
|
if (!date_is_valid(yyyymmdd) || !out)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
memcpy(cur, yyyymmdd, 8);
|
||||||
|
cur[8] = '\0';
|
||||||
|
|
||||||
|
do {
|
||||||
|
add_one_day(cur, cur);
|
||||||
|
if (++guard > 14) /* 무한루프 방지 */
|
||||||
|
return -1;
|
||||||
|
} while (date_is_weekend(cur));
|
||||||
|
|
||||||
|
memcpy(out, cur, 8);
|
||||||
|
out[8] = '\0';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void date_today(char *out)
|
||||||
|
{
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm tmv;
|
||||||
|
localtime_r(&now, &tmv);
|
||||||
|
strftime(out, 9, "%Y%m%d", &tmv);
|
||||||
|
}
|
||||||
109
legacy/app/common/util_msg.c
Normal file
109
legacy/app/common/util_msg.c
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* util_msg.c - 고정길이 전문 pack/unpack 유틸리티 (plain C)
|
||||||
|
*/
|
||||||
|
#include "acq_util.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
void msg_field_copy(char *out, const char *field, int len)
|
||||||
|
{
|
||||||
|
int end;
|
||||||
|
|
||||||
|
if (!out || !field || len < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* 우측 공백 제거하여 널 종료 문자열 생성 */
|
||||||
|
end = len;
|
||||||
|
while (end > 0 && (field[end - 1] == ' ' || field[end - 1] == '\0'))
|
||||||
|
end--;
|
||||||
|
|
||||||
|
memcpy(out, field, end);
|
||||||
|
out[end] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
void msg_field_set(char *field, const char *src, int len)
|
||||||
|
{
|
||||||
|
int slen, i;
|
||||||
|
|
||||||
|
if (!field || len < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
slen = src ? (int)strlen(src) : 0;
|
||||||
|
if (slen > len)
|
||||||
|
slen = len;
|
||||||
|
|
||||||
|
memcpy(field, src, slen);
|
||||||
|
for (i = slen; i < len; i++) /* 우측 공백 패딩 */
|
||||||
|
field[i] = ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
void msg_field_set_num(char *field, const char *src, int len)
|
||||||
|
{
|
||||||
|
int slen, pad, i;
|
||||||
|
|
||||||
|
if (!field || len < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
slen = src ? (int)strlen(src) : 0;
|
||||||
|
if (slen > len)
|
||||||
|
slen = len;
|
||||||
|
|
||||||
|
pad = len - slen;
|
||||||
|
for (i = 0; i < pad; i++) /* 좌측 zero 패딩 */
|
||||||
|
field[i] = '0';
|
||||||
|
if (src)
|
||||||
|
memcpy(field + pad, src, slen);
|
||||||
|
}
|
||||||
|
|
||||||
|
int msg_unpack(acq_msg_t *msg, const char *raw, int rawlen)
|
||||||
|
{
|
||||||
|
if (!msg || !raw)
|
||||||
|
return -1;
|
||||||
|
if (rawlen < MSG_ACQ_LEN)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
/* 위치기반 레이아웃이 곧 구조체 메모리 배열과 동일 */
|
||||||
|
memcpy(msg, raw, MSG_ACQ_LEN);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int msg_pack(const acq_msg_t *msg, char *raw, int rawlen)
|
||||||
|
{
|
||||||
|
if (!msg || !raw)
|
||||||
|
return -1;
|
||||||
|
if (rawlen < MSG_ACQ_LEN)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
memcpy(raw, msg, MSG_ACQ_LEN);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int field_blank(const char *field, int len)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
if (field[i] != ' ' && field[i] != '\0')
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int msg_validate(const acq_msg_t *msg)
|
||||||
|
{
|
||||||
|
if (!msg)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (field_blank(msg->msg_type, FLD_MSG_TYPE_LEN))
|
||||||
|
return -1;
|
||||||
|
if (field_blank(msg->merch_id, FLD_MERCH_ID_LEN))
|
||||||
|
return -1;
|
||||||
|
if (field_blank(msg->card_no, FLD_CARD_NO_LEN))
|
||||||
|
return -1;
|
||||||
|
if (field_blank(msg->amount, FLD_AMOUNT_LEN))
|
||||||
|
return -1;
|
||||||
|
if (field_blank(msg->txn_date, FLD_TXN_DATE_LEN))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
206
legacy/app/dbio/purchase_dbio.pgc
Normal file
206
legacy/app/dbio/purchase_dbio.pgc
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
/*
|
||||||
|
* purchase_dbio.pgc - 매입/정산 DBIO (ECPG 임베디드 SQL)
|
||||||
|
*
|
||||||
|
* Pro*C 상당의 PostgreSQL ECPG 로 매입/승인/정산 테이블에 대한
|
||||||
|
* INSERT/SELECT/UPDATE 를 캡슐화한다. 서비스/배치는 이 함수만 호출.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "txcore.h"
|
||||||
|
#include "txcore_dbio.h"
|
||||||
|
#include "purchase_dbio.h"
|
||||||
|
|
||||||
|
EXEC SQL INCLUDE sqlca;
|
||||||
|
|
||||||
|
/* 컴파일 시 접속하지 않지만, 실환경 접속 규약을 그대로 둔다. */
|
||||||
|
int dbio_connect(const char *target)
|
||||||
|
{
|
||||||
|
EXEC SQL BEGIN DECLARE SECTION;
|
||||||
|
const char *h_target;
|
||||||
|
EXEC SQL END DECLARE SECTION;
|
||||||
|
|
||||||
|
h_target = (target && target[0]) ? target : "acquire@localhost";
|
||||||
|
|
||||||
|
EXEC SQL CONNECT TO :h_target;
|
||||||
|
if (sqlca.sqlcode != TX_SQL_OK) {
|
||||||
|
TX_DBIO_LOG_ERR("CONNECT");
|
||||||
|
return TX_EDB;
|
||||||
|
}
|
||||||
|
tx_log(TX_LOG_INFO, "DB 접속 완료 target=%s", h_target);
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dbio_disconnect(void)
|
||||||
|
{
|
||||||
|
EXEC SQL DISCONNECT CURRENT;
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int purchase_insert(const purchase_rec_t *rec)
|
||||||
|
{
|
||||||
|
EXEC SQL BEGIN DECLARE SECTION;
|
||||||
|
char h_appr_no[10];
|
||||||
|
char h_merch_id[16];
|
||||||
|
char h_card_no[17];
|
||||||
|
long h_amount;
|
||||||
|
char h_txn_date[9];
|
||||||
|
char h_status[2];
|
||||||
|
EXEC SQL END DECLARE SECTION;
|
||||||
|
|
||||||
|
if (!rec)
|
||||||
|
return TX_EINVAL;
|
||||||
|
|
||||||
|
strncpy(h_appr_no, rec->appr_no, sizeof(h_appr_no) - 1);
|
||||||
|
h_appr_no[sizeof(h_appr_no) - 1] = '\0';
|
||||||
|
strncpy(h_merch_id, rec->merch_id, sizeof(h_merch_id) - 1);
|
||||||
|
h_merch_id[sizeof(h_merch_id) - 1] = '\0';
|
||||||
|
strncpy(h_card_no, rec->card_no, sizeof(h_card_no) - 1);
|
||||||
|
h_card_no[sizeof(h_card_no) - 1] = '\0';
|
||||||
|
strncpy(h_txn_date, rec->txn_date, sizeof(h_txn_date) - 1);
|
||||||
|
h_txn_date[sizeof(h_txn_date) - 1] = '\0';
|
||||||
|
strncpy(h_status, rec->status, sizeof(h_status) - 1);
|
||||||
|
h_status[sizeof(h_status) - 1] = '\0';
|
||||||
|
h_amount = rec->amount;
|
||||||
|
|
||||||
|
EXEC SQL INSERT INTO purchase
|
||||||
|
(appr_no, merch_id, card_no, amount, txn_date, status)
|
||||||
|
VALUES
|
||||||
|
(:h_appr_no, :h_merch_id, :h_card_no, :h_amount, :h_txn_date, :h_status);
|
||||||
|
|
||||||
|
if (sqlca.sqlcode != TX_SQL_OK) {
|
||||||
|
TX_DBIO_LOG_ERR("purchase_insert");
|
||||||
|
return TX_EDB;
|
||||||
|
}
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int purchase_select_by_appr(const char *appr_no, purchase_rec_t *out)
|
||||||
|
{
|
||||||
|
EXEC SQL BEGIN DECLARE SECTION;
|
||||||
|
char h_appr_no[10];
|
||||||
|
char h_merch_id[16];
|
||||||
|
char h_card_no[17];
|
||||||
|
long h_amount;
|
||||||
|
char h_txn_date[9];
|
||||||
|
char h_status[2];
|
||||||
|
EXEC SQL END DECLARE SECTION;
|
||||||
|
|
||||||
|
if (!appr_no || !out)
|
||||||
|
return TX_EINVAL;
|
||||||
|
|
||||||
|
strncpy(h_appr_no, appr_no, sizeof(h_appr_no) - 1);
|
||||||
|
h_appr_no[sizeof(h_appr_no) - 1] = '\0';
|
||||||
|
|
||||||
|
EXEC SQL SELECT merch_id, card_no, amount, txn_date, status
|
||||||
|
INTO :h_merch_id, :h_card_no, :h_amount, :h_txn_date, :h_status
|
||||||
|
FROM purchase
|
||||||
|
WHERE appr_no = :h_appr_no;
|
||||||
|
|
||||||
|
if (sqlca.sqlcode == TX_SQL_NOTFOUND)
|
||||||
|
return TX_ENOENT;
|
||||||
|
if (sqlca.sqlcode != TX_SQL_OK) {
|
||||||
|
TX_DBIO_LOG_ERR("purchase_select_by_appr");
|
||||||
|
return TX_EDB;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
strncpy(out->appr_no, h_appr_no, sizeof(out->appr_no) - 1);
|
||||||
|
strncpy(out->merch_id, h_merch_id, sizeof(out->merch_id) - 1);
|
||||||
|
strncpy(out->card_no, h_card_no, sizeof(out->card_no) - 1);
|
||||||
|
strncpy(out->txn_date, h_txn_date, sizeof(out->txn_date) - 1);
|
||||||
|
strncpy(out->status, h_status, sizeof(out->status) - 1);
|
||||||
|
out->amount = h_amount;
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int purchase_update_status(const char *appr_no, const char *status)
|
||||||
|
{
|
||||||
|
EXEC SQL BEGIN DECLARE SECTION;
|
||||||
|
char h_appr_no[10];
|
||||||
|
char h_status[2];
|
||||||
|
EXEC SQL END DECLARE SECTION;
|
||||||
|
|
||||||
|
if (!appr_no || !status)
|
||||||
|
return TX_EINVAL;
|
||||||
|
|
||||||
|
strncpy(h_appr_no, appr_no, sizeof(h_appr_no) - 1);
|
||||||
|
h_appr_no[sizeof(h_appr_no) - 1] = '\0';
|
||||||
|
strncpy(h_status, status, sizeof(h_status) - 1);
|
||||||
|
h_status[sizeof(h_status) - 1] = '\0';
|
||||||
|
|
||||||
|
EXEC SQL UPDATE purchase
|
||||||
|
SET status = :h_status, upd_ts = now()
|
||||||
|
WHERE appr_no = :h_appr_no;
|
||||||
|
|
||||||
|
if (sqlca.sqlcode != TX_SQL_OK) {
|
||||||
|
TX_DBIO_LOG_ERR("purchase_update_status");
|
||||||
|
return TX_EDB;
|
||||||
|
}
|
||||||
|
if (sqlca.sqlerrd[2] == 0) /* 갱신된 행 수 0 = 대상 없음 */
|
||||||
|
return TX_ENOENT;
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int approval_match(const char *appr_no, long amount)
|
||||||
|
{
|
||||||
|
EXEC SQL BEGIN DECLARE SECTION;
|
||||||
|
char h_appr_no[10];
|
||||||
|
long h_amount;
|
||||||
|
int h_cnt;
|
||||||
|
EXEC SQL END DECLARE SECTION;
|
||||||
|
|
||||||
|
if (!appr_no)
|
||||||
|
return TX_EINVAL;
|
||||||
|
|
||||||
|
strncpy(h_appr_no, appr_no, sizeof(h_appr_no) - 1);
|
||||||
|
h_appr_no[sizeof(h_appr_no) - 1] = '\0';
|
||||||
|
h_amount = amount;
|
||||||
|
|
||||||
|
EXEC SQL SELECT count(*)
|
||||||
|
INTO :h_cnt
|
||||||
|
FROM approval
|
||||||
|
WHERE appr_no = :h_appr_no
|
||||||
|
AND amount = :h_amount
|
||||||
|
AND status = 'A';
|
||||||
|
|
||||||
|
if (sqlca.sqlcode != TX_SQL_OK) {
|
||||||
|
TX_DBIO_LOG_ERR("approval_match");
|
||||||
|
return TX_EDB;
|
||||||
|
}
|
||||||
|
return (h_cnt > 0) ? TX_OK : TX_ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int settlement_accumulate(const char *merch_id, const char *biz_date,
|
||||||
|
long amount)
|
||||||
|
{
|
||||||
|
EXEC SQL BEGIN DECLARE SECTION;
|
||||||
|
char h_merch_id[16];
|
||||||
|
char h_biz_date[9];
|
||||||
|
long h_amount;
|
||||||
|
EXEC SQL END DECLARE SECTION;
|
||||||
|
|
||||||
|
if (!merch_id || !biz_date)
|
||||||
|
return TX_EINVAL;
|
||||||
|
|
||||||
|
strncpy(h_merch_id, merch_id, sizeof(h_merch_id) - 1);
|
||||||
|
h_merch_id[sizeof(h_merch_id) - 1] = '\0';
|
||||||
|
strncpy(h_biz_date, biz_date, sizeof(h_biz_date) - 1);
|
||||||
|
h_biz_date[sizeof(h_biz_date) - 1] = '\0';
|
||||||
|
h_amount = amount;
|
||||||
|
|
||||||
|
/* 가맹점/영업일 단위 정산집계 upsert */
|
||||||
|
EXEC SQL INSERT INTO settlement
|
||||||
|
(merch_id, biz_date, total_amount, txn_cnt)
|
||||||
|
VALUES
|
||||||
|
(:h_merch_id, :h_biz_date, :h_amount, 1)
|
||||||
|
ON CONFLICT (merch_id, biz_date) DO UPDATE
|
||||||
|
SET total_amount = settlement.total_amount + EXCLUDED.total_amount,
|
||||||
|
txn_cnt = settlement.txn_cnt + 1;
|
||||||
|
|
||||||
|
if (sqlca.sqlcode != TX_SQL_OK) {
|
||||||
|
TX_DBIO_LOG_ERR("settlement_accumulate");
|
||||||
|
return TX_EDB;
|
||||||
|
}
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
45
legacy/app/include/acq_util.h
Normal file
45
legacy/app/include/acq_util.h
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* acq_util.h - 공통 유틸리티 (일자/금액/전문) 선언
|
||||||
|
*/
|
||||||
|
#ifndef ACQ_UTIL_H
|
||||||
|
#define ACQ_UTIL_H
|
||||||
|
|
||||||
|
#include "msg_layout.h"
|
||||||
|
|
||||||
|
/* ---- util_date.c : 일자/영업일 ---- */
|
||||||
|
/* YYYYMMDD 형식 유효성 검사. 유효=1, 아니면 0 */
|
||||||
|
int date_is_valid(const char *yyyymmdd);
|
||||||
|
/* 요일 계산 (0=일 ... 6=토). 실패 시 -1 */
|
||||||
|
int date_weekday(const char *yyyymmdd);
|
||||||
|
/* 주말(토/일) 여부. 1=주말 */
|
||||||
|
int date_is_weekend(const char *yyyymmdd);
|
||||||
|
/* 다음 영업일(주말 건너뜀)을 out(YYYYMMDD, 최소 9바이트)에 기록. 0=성공 */
|
||||||
|
int date_next_business(const char *yyyymmdd, char *out);
|
||||||
|
/* 오늘 일자를 YYYYMMDD 로 out 에 기록 */
|
||||||
|
void date_today(char *out);
|
||||||
|
|
||||||
|
/* ---- util_amount.c : 금액/통화 ---- */
|
||||||
|
/* zero-pad 금액 문자열(len 폭)을 long 으로 파싱. 실패 시 <0 */
|
||||||
|
long amount_parse(const char *field, int len);
|
||||||
|
/* long 금액을 폭 len 의 zero-pad 문자열로 out 에 기록. 0=성공 */
|
||||||
|
int amount_format(long amount, char *out, int len);
|
||||||
|
/* 원화 콤마 포맷 (예: 12500 -> "12,500"). out 은 32바이트 권장 */
|
||||||
|
void amount_format_won(long amount, char *out, int outlen);
|
||||||
|
/* 금액 범위 검증 (1 이상, 한도 이하). 1=유효 */
|
||||||
|
int amount_is_valid(long amount);
|
||||||
|
|
||||||
|
/* ---- util_msg.c : 전문 pack/unpack ---- */
|
||||||
|
/* 고정폭 필드를 널종료 문자열 out 으로 복사(우측 공백 trim). */
|
||||||
|
void msg_field_copy(char *out, const char *field, int len);
|
||||||
|
/* 널종료 문자열 src 를 폭 len 필드에 좌측정렬/우측공백으로 기록 */
|
||||||
|
void msg_field_set(char *field, const char *src, int len);
|
||||||
|
/* 숫자 문자열 src 를 폭 len 필드에 zero-pad 로 기록 */
|
||||||
|
void msg_field_set_num(char *field, const char *src, int len);
|
||||||
|
/* 100바이트 raw 버퍼를 acq_msg_t 로 매핑(길이검증). 0=성공 */
|
||||||
|
int msg_unpack(acq_msg_t *msg, const char *raw, int rawlen);
|
||||||
|
/* acq_msg_t 를 raw(최소 MSG_ACQ_LEN) 로 직렬화. 0=성공 */
|
||||||
|
int msg_pack(const acq_msg_t *msg, char *raw, int rawlen);
|
||||||
|
/* 필수항목(전문구분/가맹점/카드/금액/일자) 존재 검증. 0=성공 */
|
||||||
|
int msg_validate(const acq_msg_t *msg);
|
||||||
|
|
||||||
|
#endif /* ACQ_UTIL_H */
|
||||||
54
legacy/app/include/msg_layout.h
Normal file
54
legacy/app/include/msg_layout.h
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* msg_layout.h - 카드 매입 전문(電文) 고정길이 레이아웃
|
||||||
|
*
|
||||||
|
* 대외/대내 전문은 고정길이 위치기반(positional) 구조다. 각 필드는
|
||||||
|
* 널 종료가 없는 고정폭 char 배열이며, 숫자 필드는 좌측 zero-padding
|
||||||
|
* (예: 거래금액 000000012500). util_msg.c 가 pack/unpack 을 담당한다.
|
||||||
|
*/
|
||||||
|
#ifndef MSG_LAYOUT_H
|
||||||
|
#define MSG_LAYOUT_H
|
||||||
|
|
||||||
|
/* 전문구분 코드 */
|
||||||
|
#define MSG_TYPE_RECV "0200" /* 매입요청(수신) */
|
||||||
|
#define MSG_TYPE_RESP "0210" /* 매입응답 */
|
||||||
|
#define MSG_TYPE_RECON "0500" /* 정산/대사 */
|
||||||
|
|
||||||
|
/* 응답코드 */
|
||||||
|
#define RESP_OK "0000" /* 정상 */
|
||||||
|
#define RESP_INVALID "9001" /* 항목오류 */
|
||||||
|
#define RESP_NOMERCH "9002" /* 가맹점 없음 */
|
||||||
|
#define RESP_SYSERR "9999" /* 시스템 오류 */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 매입 요청 전문 헤더 + 바디 (총 MSG_ACQ_LEN 바이트)
|
||||||
|
* 필드 순서/폭이 곧 와이어 포맷이므로 재배치 금지.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
char msg_type[4]; /* 전문구분 4 */
|
||||||
|
char trans_code[6]; /* 거래코드 6 */
|
||||||
|
char merch_id[15]; /* 가맹점번호 15 */
|
||||||
|
char card_no[16]; /* 카드번호(마스킹) 16 */
|
||||||
|
char approval_no[9]; /* 승인번호 9 */
|
||||||
|
char amount[12]; /* 거래금액 12 (zero-pad, 원) */
|
||||||
|
char txn_date[8]; /* 거래일자 8 (YYYYMMDD) */
|
||||||
|
char txn_time[6]; /* 거래시각 6 (HHMMSS) */
|
||||||
|
char inst_month[2]; /* 할부개월 2 (00=일시불) */
|
||||||
|
char resp_code[4]; /* 응답코드 4 */
|
||||||
|
char filler[18]; /* 예비 18 */
|
||||||
|
} acq_msg_t; /* 합계 100 바이트 */
|
||||||
|
|
||||||
|
#define MSG_ACQ_LEN ((int)sizeof(acq_msg_t)) /* = 100 */
|
||||||
|
|
||||||
|
/* 필드 폭 상수 (unpack 검증용) */
|
||||||
|
#define FLD_MSG_TYPE_LEN 4
|
||||||
|
#define FLD_TRANS_CODE_LEN 6
|
||||||
|
#define FLD_MERCH_ID_LEN 15
|
||||||
|
#define FLD_CARD_NO_LEN 16
|
||||||
|
#define FLD_APPROVAL_LEN 9
|
||||||
|
#define FLD_AMOUNT_LEN 12
|
||||||
|
#define FLD_TXN_DATE_LEN 8
|
||||||
|
#define FLD_TXN_TIME_LEN 6
|
||||||
|
#define FLD_INST_LEN 2
|
||||||
|
#define FLD_RESP_LEN 4
|
||||||
|
|
||||||
|
#endif /* MSG_LAYOUT_H */
|
||||||
47
legacy/app/include/purchase_dbio.h
Normal file
47
legacy/app/include/purchase_dbio.h
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* purchase_dbio.h - 매입/정산 DBIO 인터페이스
|
||||||
|
*
|
||||||
|
* 서비스/배치 코드는 SQL 을 직접 다루지 않고 이 함수들을 호출한다.
|
||||||
|
* 구현은 app/dbio/purchase_dbio.pgc (ECPG).
|
||||||
|
*/
|
||||||
|
#ifndef PURCHASE_DBIO_H
|
||||||
|
#define PURCHASE_DBIO_H
|
||||||
|
|
||||||
|
/* 매입 상태 코드 */
|
||||||
|
#define PUR_ST_RECV "R" /* 접수(수신) */
|
||||||
|
#define PUR_ST_MATCHED "M" /* 대사완료 */
|
||||||
|
#define PUR_ST_UNMATCH "U" /* 대사불일치 */
|
||||||
|
#define PUR_ST_SETTLED "S" /* 정산완료 */
|
||||||
|
|
||||||
|
/* 매입 레코드 (purchase 테이블 1행) */
|
||||||
|
typedef struct {
|
||||||
|
char appr_no[10]; /* 승인번호 PK */
|
||||||
|
char merch_id[16]; /* 가맹점번호 */
|
||||||
|
char card_no[17]; /* 카드번호 */
|
||||||
|
long amount; /* 거래금액(원) */
|
||||||
|
char txn_date[9]; /* 거래일자 YYYYMMDD */
|
||||||
|
char status[2]; /* 상태코드 */
|
||||||
|
} purchase_rec_t;
|
||||||
|
|
||||||
|
/* DB 연결/해제 (배치/데몬 기동 시). target 은 "db@host" 또는 NULL */
|
||||||
|
int dbio_connect(const char *target);
|
||||||
|
int dbio_disconnect(void);
|
||||||
|
|
||||||
|
/* 매입 접수 INSERT. 0=성공 */
|
||||||
|
int purchase_insert(const purchase_rec_t *rec);
|
||||||
|
/* 승인번호로 단건 조회. 0=성공, TX_ENOENT=없음 */
|
||||||
|
int purchase_select_by_appr(const char *appr_no, purchase_rec_t *out);
|
||||||
|
/* 상태 갱신. 0=성공 */
|
||||||
|
int purchase_update_status(const char *appr_no, const char *status);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 승인원장(approval) 에서 승인번호+금액 일치 여부 확인.
|
||||||
|
* 0=일치, TX_ENOENT=불일치/없음.
|
||||||
|
*/
|
||||||
|
int approval_match(const char *appr_no, long amount);
|
||||||
|
|
||||||
|
/* 정산집계(settlement) upsert. 가맹점/일자별 금액 누적. 0=성공 */
|
||||||
|
int settlement_accumulate(const char *merch_id, const char *biz_date,
|
||||||
|
long amount);
|
||||||
|
|
||||||
|
#endif /* PURCHASE_DBIO_H */
|
||||||
136
legacy/app/online/ac_intake_svc.pgc
Normal file
136
legacy/app/online/ac_intake_svc.pgc
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* ac_intake_svc.pgc - 매입접수 서비스 (AC_INTAKE)
|
||||||
|
*
|
||||||
|
* MG_RECV 가 적재한 TXBUF 필드를 검증하고, 트랜잭션 하에서
|
||||||
|
* purchase 테이블에 접수 INSERT 한 뒤 정산집계 서비스를 tx_call 한다.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "txcore.h"
|
||||||
|
#include "acq_util.h"
|
||||||
|
#include "purchase_dbio.h"
|
||||||
|
|
||||||
|
EXEC SQL INCLUDE sqlca;
|
||||||
|
|
||||||
|
TX_SERVICE(AC_INTAKE, ctx)
|
||||||
|
{
|
||||||
|
purchase_rec_t rec;
|
||||||
|
char merch[16];
|
||||||
|
char card[20];
|
||||||
|
char appr[16];
|
||||||
|
char txndate[16];
|
||||||
|
long amount = 0;
|
||||||
|
int rc;
|
||||||
|
TXBUF *sub_in;
|
||||||
|
TXBUF *sub_out;
|
||||||
|
|
||||||
|
tx_log(TX_LOG_INFO, "[AC_INTAKE] 매입접수 서비스 진입");
|
||||||
|
|
||||||
|
/* 입력 필드 추출 */
|
||||||
|
if (tx_buf_get(ctx->in, "MERCHID", merch, sizeof(merch)) != TX_OK ||
|
||||||
|
tx_buf_get(ctx->in, "CARDNO", card, sizeof(card)) != TX_OK ||
|
||||||
|
tx_buf_get(ctx->in, "APPRNO", appr, sizeof(appr)) != TX_OK ||
|
||||||
|
tx_buf_get(ctx->in, "TXNDATE", txndate, sizeof(txndate)) != TX_OK) {
|
||||||
|
tx_log(TX_LOG_ERROR, "[AC_INTAKE] 입력 필드 누락");
|
||||||
|
tx_return(ctx, TX_EINVAL, ctx->out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tx_buf_getlong(ctx->in, "AMOUNT", &amount);
|
||||||
|
|
||||||
|
/* 업무 검증: 가맹점/금액/일자 */
|
||||||
|
if (merch[0] == '\0') {
|
||||||
|
tx_log(TX_LOG_WARN, "[AC_INTAKE] 가맹점번호 없음");
|
||||||
|
tx_return(ctx, TX_EINVAL, ctx->out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!amount_is_valid(amount)) {
|
||||||
|
tx_log(TX_LOG_WARN, "[AC_INTAKE] 금액 범위 오류 amount=%ld", amount);
|
||||||
|
tx_return(ctx, TX_EINVAL, ctx->out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!date_is_valid(txndate)) {
|
||||||
|
tx_log(TX_LOG_WARN, "[AC_INTAKE] 거래일자 오류 date=%s", txndate);
|
||||||
|
tx_return(ctx, TX_EINVAL, ctx->out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 레코드 구성 */
|
||||||
|
memset(&rec, 0, sizeof(rec));
|
||||||
|
strncpy(rec.appr_no, appr, sizeof(rec.appr_no) - 1);
|
||||||
|
strncpy(rec.merch_id, merch, sizeof(rec.merch_id) - 1);
|
||||||
|
strncpy(rec.card_no, card, sizeof(rec.card_no) - 1);
|
||||||
|
strncpy(rec.txn_date, txndate, sizeof(rec.txn_date) - 1);
|
||||||
|
strncpy(rec.status, PUR_ST_RECV, sizeof(rec.status) - 1);
|
||||||
|
rec.amount = amount;
|
||||||
|
|
||||||
|
/* 트랜잭션 시작 → 접수 INSERT */
|
||||||
|
if (tx_begin() != TX_OK) {
|
||||||
|
tx_return(ctx, TX_FAIL, ctx->out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = purchase_insert(&rec);
|
||||||
|
if (rc != TX_OK) {
|
||||||
|
tx_log(TX_LOG_ERROR, "[AC_INTAKE] 접수 INSERT 실패 rc=%d(%s)",
|
||||||
|
rc, tx_strerror(rc));
|
||||||
|
tx_abort();
|
||||||
|
tx_return(ctx, rc, ctx->out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 정산집계 서비스 호출 (동일 트랜잭션 컨텍스트) */
|
||||||
|
sub_in = tx_buf_alloc();
|
||||||
|
sub_out = tx_buf_alloc();
|
||||||
|
if (sub_in && sub_out) {
|
||||||
|
tx_buf_sets(sub_in, "MERCHID", merch);
|
||||||
|
tx_buf_sets(sub_in, "TXNDATE", txndate);
|
||||||
|
tx_buf_setlong(sub_in, "AMOUNT", amount);
|
||||||
|
rc = tx_call("AC_SETTLE", sub_in, sub_out);
|
||||||
|
if (rc != TX_OK)
|
||||||
|
tx_log(TX_LOG_WARN, "[AC_INTAKE] 정산집계 경고 rc=%d", rc);
|
||||||
|
}
|
||||||
|
tx_buf_free(sub_in);
|
||||||
|
tx_buf_free(sub_out);
|
||||||
|
|
||||||
|
tx_commit();
|
||||||
|
|
||||||
|
tx_buf_reset(ctx->out);
|
||||||
|
tx_buf_sets(ctx->out, "RESPCODE", RESP_OK);
|
||||||
|
tx_buf_sets(ctx->out, "APPRNO", appr);
|
||||||
|
tx_log(TX_LOG_INFO, "[AC_INTAKE] 접수완료 appr=%s amount=%ld", appr, amount);
|
||||||
|
|
||||||
|
tx_return(ctx, TX_OK, ctx->out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AC_SETTLE - 정산집계 서비스 (동일 모듈에 배치)
|
||||||
|
* purchase 접수 건을 가맹점/영업일 단위 정산집계에 누적한다.
|
||||||
|
*/
|
||||||
|
TX_SERVICE(AC_SETTLE, ctx)
|
||||||
|
{
|
||||||
|
char merch[16];
|
||||||
|
char txndate[16];
|
||||||
|
char bizdate[9];
|
||||||
|
long amount = 0;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
if (tx_buf_get(ctx->in, "MERCHID", merch, sizeof(merch)) != TX_OK ||
|
||||||
|
tx_buf_get(ctx->in, "TXNDATE", txndate, sizeof(txndate)) != TX_OK) {
|
||||||
|
tx_return(ctx, TX_EINVAL, ctx->out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tx_buf_getlong(ctx->in, "AMOUNT", &amount);
|
||||||
|
|
||||||
|
/* 정산 영업일 = 거래일의 다음 영업일 */
|
||||||
|
if (date_next_business(txndate, bizdate) != 0) {
|
||||||
|
memcpy(bizdate, txndate, 8);
|
||||||
|
bizdate[8] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = settlement_accumulate(merch, bizdate, amount);
|
||||||
|
tx_log(TX_LOG_INFO, "[AC_SETTLE] 집계 merch=%s biz=%s amount=%ld rc=%d",
|
||||||
|
merch, bizdate, amount, rc);
|
||||||
|
|
||||||
|
tx_return(ctx, rc, ctx->out);
|
||||||
|
}
|
||||||
71
legacy/app/online/mg_recv_svc.pgc
Normal file
71
legacy/app/online/mg_recv_svc.pgc
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* mg_recv_svc.pgc - 전문수신 서비스 (MG_RECV)
|
||||||
|
*
|
||||||
|
* 대외 매입요청 전문(고정길이 100바이트)을 수신하여 unpack 하고,
|
||||||
|
* 필드를 TXBUF 로 적재한 뒤 후속 서비스가 소비하도록 응답한다.
|
||||||
|
* (ATMI 의 void SVC(TPSVCINFO*) 대응)
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "txcore.h"
|
||||||
|
#include "acq_util.h"
|
||||||
|
#include "purchase_dbio.h"
|
||||||
|
|
||||||
|
EXEC SQL INCLUDE sqlca;
|
||||||
|
|
||||||
|
TX_SERVICE(MG_RECV, ctx)
|
||||||
|
{
|
||||||
|
char raw[MSG_ACQ_LEN + 1];
|
||||||
|
acq_msg_t msg;
|
||||||
|
char fld[64];
|
||||||
|
long amount;
|
||||||
|
int rc;
|
||||||
|
TXBUF *out;
|
||||||
|
|
||||||
|
tx_log(TX_LOG_INFO, "[MG_RECV] 전문수신 서비스 진입");
|
||||||
|
|
||||||
|
/* 요청 버퍼에서 원시 전문 획득 */
|
||||||
|
memset(raw, 0, sizeof(raw));
|
||||||
|
rc = tx_buf_get(ctx->in, "RAWMSG", raw, sizeof(raw));
|
||||||
|
if (rc != TX_OK) {
|
||||||
|
tx_log(TX_LOG_ERROR, "[MG_RECV] RAWMSG 누락 rc=%d", rc);
|
||||||
|
tx_return(ctx, TX_EINVAL, ctx->out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg_unpack(&msg, raw, (int)strlen(raw)) != 0) {
|
||||||
|
tx_log(TX_LOG_ERROR, "[MG_RECV] 전문 언팩 실패 len=%zu", strlen(raw));
|
||||||
|
tx_return(ctx, TX_EINVAL, ctx->out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg_validate(&msg) != 0) {
|
||||||
|
tx_log(TX_LOG_WARN, "[MG_RECV] 필수항목 검증 실패");
|
||||||
|
tx_return(ctx, TX_EINVAL, ctx->out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 필드를 응답 TXBUF 로 적재 (후속 매입접수 서비스가 소비) */
|
||||||
|
out = ctx->out;
|
||||||
|
tx_buf_reset(out);
|
||||||
|
|
||||||
|
msg_field_copy(fld, msg.msg_type, FLD_MSG_TYPE_LEN);
|
||||||
|
tx_buf_sets(out, "MSGTYPE", fld);
|
||||||
|
msg_field_copy(fld, msg.merch_id, FLD_MERCH_ID_LEN);
|
||||||
|
tx_buf_sets(out, "MERCHID", fld);
|
||||||
|
msg_field_copy(fld, msg.card_no, FLD_CARD_NO_LEN);
|
||||||
|
tx_buf_sets(out, "CARDNO", fld);
|
||||||
|
msg_field_copy(fld, msg.approval_no, FLD_APPROVAL_LEN);
|
||||||
|
tx_buf_sets(out, "APPRNO", fld);
|
||||||
|
msg_field_copy(fld, msg.txn_date, FLD_TXN_DATE_LEN);
|
||||||
|
tx_buf_sets(out, "TXNDATE", fld);
|
||||||
|
|
||||||
|
amount = amount_parse(msg.amount, FLD_AMOUNT_LEN);
|
||||||
|
tx_buf_setlong(out, "AMOUNT", amount);
|
||||||
|
|
||||||
|
tx_log(TX_LOG_INFO, "[MG_RECV] 언팩완료 merch=%.15s appr=%.9s amount=%ld",
|
||||||
|
msg.merch_id, msg.approval_no, amount);
|
||||||
|
|
||||||
|
tx_return(ctx, TX_OK, out);
|
||||||
|
}
|
||||||
36
legacy/app/online/server_main.c
Normal file
36
legacy/app/online/server_main.c
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* server_main.c - acquire-core 온라인 서버 데모 부트스트랩
|
||||||
|
*
|
||||||
|
* Tuxedo 의 tmboot/서버 메인처럼, 온라인 서비스들을 TxCore 레지스트리에
|
||||||
|
* 등록한다. 실제 TP 모니터에서는 tpsvrinit() 에 해당한다. 여기서는
|
||||||
|
* 등록/디스패치 링크 경로를 검증하기 위한 최소 데모 엔트리다.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "txcore.h"
|
||||||
|
|
||||||
|
/* 각 .pgc 서비스의 외부 선언 (TX_SERVICE 매크로가 생성한 함수) */
|
||||||
|
void MG_RECV(TXSVCINFO *ctx);
|
||||||
|
void AC_INTAKE(TXSVCINFO *ctx);
|
||||||
|
void AC_SETTLE(TXSVCINFO *ctx);
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
tx_set_loglevel(TX_LOG_INFO);
|
||||||
|
tx_log(TX_LOG_INFO, "acquire-core 온라인 서버 기동 (TxCore)");
|
||||||
|
|
||||||
|
/* 서비스 등록 (tpsvrinit 상당) */
|
||||||
|
tx_register("MG_RECV", MG_RECV);
|
||||||
|
tx_register("AC_INTAKE", AC_INTAKE);
|
||||||
|
tx_register("AC_SETTLE", AC_SETTLE);
|
||||||
|
|
||||||
|
tx_log(TX_LOG_INFO, "등록 서비스 수 = %d", tx_service_count());
|
||||||
|
printf("acquire-core-server 준비완료: 서비스 %d개 등록\n",
|
||||||
|
tx_service_count());
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 실환경에서는 여기서 TP 모니터 이벤트 루프(advertise/serve)로 진입한다.
|
||||||
|
* 데모에서는 DB 접속 없이 링크 경로만 검증하고 종료한다.
|
||||||
|
*/
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
41
legacy/db/schema.sql
Normal file
41
legacy/db/schema.sql
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
-- schema.sql - acquire-core 매입/정산 스키마
|
||||||
|
--
|
||||||
|
-- ECPG DBIO 코드가 참조하는 테이블 정의. 컴파일에는 필요 없으며,
|
||||||
|
-- 실행(런타임) 시 PostgreSQL 에 적용한다.
|
||||||
|
|
||||||
|
-- 매입 원장 (전문수신 → 접수)
|
||||||
|
CREATE TABLE IF NOT EXISTS purchase (
|
||||||
|
appr_no VARCHAR(9) NOT NULL, -- 승인번호 (PK)
|
||||||
|
merch_id VARCHAR(15) NOT NULL, -- 가맹점번호
|
||||||
|
card_no VARCHAR(16) NOT NULL, -- 카드번호(마스킹)
|
||||||
|
amount BIGINT NOT NULL, -- 거래금액(원)
|
||||||
|
txn_date CHAR(8) NOT NULL, -- 거래일자 YYYYMMDD
|
||||||
|
status CHAR(1) NOT NULL DEFAULT 'R', -- R:접수 M:대사완료 U:불일치 S:정산완료
|
||||||
|
reg_ts TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
upd_ts TIMESTAMP,
|
||||||
|
CONSTRAINT pk_purchase PRIMARY KEY (appr_no)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_purchase_date_status
|
||||||
|
ON purchase (txn_date, status);
|
||||||
|
|
||||||
|
-- 승인 원장 (매입 대사 기준)
|
||||||
|
CREATE TABLE IF NOT EXISTS approval (
|
||||||
|
appr_no VARCHAR(9) NOT NULL,
|
||||||
|
merch_id VARCHAR(15) NOT NULL,
|
||||||
|
amount BIGINT NOT NULL,
|
||||||
|
appr_date CHAR(8) NOT NULL,
|
||||||
|
status CHAR(1) NOT NULL DEFAULT 'A', -- A:승인 C:취소
|
||||||
|
reg_ts TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT pk_approval PRIMARY KEY (appr_no)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 정산 집계 (가맹점/영업일 단위)
|
||||||
|
CREATE TABLE IF NOT EXISTS settlement (
|
||||||
|
merch_id VARCHAR(15) NOT NULL,
|
||||||
|
biz_date CHAR(8) NOT NULL, -- 정산 영업일 YYYYMMDD
|
||||||
|
total_amount BIGINT NOT NULL DEFAULT 0,
|
||||||
|
txn_cnt INTEGER NOT NULL DEFAULT 0,
|
||||||
|
upd_ts TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT pk_settlement PRIMARY KEY (merch_id, biz_date)
|
||||||
|
);
|
||||||
31
legacy/framework/txcore/dbio/txcore_dbio.h
Normal file
31
legacy/framework/txcore/dbio/txcore_dbio.h
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* txcore_dbio.h - TxCore DBIO 규약
|
||||||
|
*
|
||||||
|
* ProFrame/Pro*C 계열의 DBIO 모듈이 따르는 공통 규약. 업무 DBIO(.pgc) 는
|
||||||
|
* ECPG 로 SQL 을 임베드하되, 결과 판정과 로깅은 이 매크로/헬퍼를 통해
|
||||||
|
* 일관되게 처리한다. sqlca 는 각 .pgc 에서 EXEC SQL INCLUDE sqlca; 로 포함.
|
||||||
|
*/
|
||||||
|
#ifndef TXCORE_DBIO_H
|
||||||
|
#define TXCORE_DBIO_H
|
||||||
|
|
||||||
|
#include "txcore.h"
|
||||||
|
|
||||||
|
/* SQLCODE 관례값 (ECPG/Pro*C 공통) */
|
||||||
|
#define TX_SQL_OK 0 /* 정상 처리 */
|
||||||
|
#define TX_SQL_NOTFOUND 100 /* 조회 결과 없음 */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* DBIO 결과 판정 매크로. sqlca 가 스코프에 있어야 한다.
|
||||||
|
* 성공(0)=TX_OK, NOTFOUND(100)=TX_ENOENT, 그 외=TX_EDB 로 매핑.
|
||||||
|
*/
|
||||||
|
#define TX_DBIO_RESULT(sc) \
|
||||||
|
((sc) == TX_SQL_OK ? TX_OK : \
|
||||||
|
(sc) == TX_SQL_NOTFOUND ? TX_ENOENT : TX_EDB)
|
||||||
|
|
||||||
|
/* SQL 오류를 표준 로그로 남기는 헬퍼 (sqlca 필드 참조) */
|
||||||
|
#define TX_DBIO_LOG_ERR(tag) \
|
||||||
|
tx_log(TX_LOG_ERROR, "DBIO 오류 [%s] sqlcode=%ld msg=%.*s", \
|
||||||
|
(tag), (long)sqlca.sqlcode, \
|
||||||
|
(int)sqlca.sqlerrm.sqlerrml, sqlca.sqlerrm.sqlerrmc)
|
||||||
|
|
||||||
|
#endif /* TXCORE_DBIO_H */
|
||||||
128
legacy/framework/txcore/include/txcore.h
Normal file
128
legacy/framework/txcore/include/txcore.h
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* txcore.h - TxCore 공통 프레임워크 API
|
||||||
|
*
|
||||||
|
* TxCore 는 사내 표준 공통 프레임워크로, Tuxedo/ProFrame 계열 TP 모니터의
|
||||||
|
* ATMI(tpcall/tpreturn/tpbegin) 및 FML/UBF(Fchg/Fget) 관용구를 얇게 감싼
|
||||||
|
* 자체 구현 계층이다. 외부 TP 미들웨어(Enduro/X 등) 설치 없이 단독으로
|
||||||
|
* 빌드/링크되도록 in-process 디스패치와 고정 슬롯 버퍼를 제공한다.
|
||||||
|
*
|
||||||
|
* 업무 코드는 이 헤더의 API 만 사용하며, 실제 TP 모니터로의 이식은
|
||||||
|
* 이 구현체만 교체하면 된다.
|
||||||
|
*/
|
||||||
|
#ifndef TXCORE_H
|
||||||
|
#define TXCORE_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 에러 코드 (ATMI tperrno 대응) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
#define TX_OK 0 /* 정상 */
|
||||||
|
#define TX_FAIL (-1) /* 일반 실패 */
|
||||||
|
#define TX_ENOENT (-2) /* 서비스 미등록 */
|
||||||
|
#define TX_EINVAL (-3) /* 파라미터 오류 */
|
||||||
|
#define TX_EDB (-4) /* DB 오류 */
|
||||||
|
#define TX_ENOMEM (-5) /* 버퍼/자원 부족 */
|
||||||
|
#define TX_ENOKEY (-6) /* 버퍼 키 없음 */
|
||||||
|
|
||||||
|
/* 로그 레벨 */
|
||||||
|
#define TX_LOG_DEBUG 0
|
||||||
|
#define TX_LOG_INFO 1
|
||||||
|
#define TX_LOG_WARN 2
|
||||||
|
#define TX_LOG_ERROR 3
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* TXBUF - 키/값 고정 슬롯 버퍼 (FML/UBF Fielded buffer 대응) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
#define TX_MAX_SLOTS 64
|
||||||
|
#define TX_KEY_LEN 32
|
||||||
|
#define TX_VAL_LEN 256
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char key[TX_KEY_LEN];
|
||||||
|
char val[TX_VAL_LEN];
|
||||||
|
int len; /* val 유효 바이트 수 */
|
||||||
|
int used; /* 슬롯 사용 여부 */
|
||||||
|
} TXFIELD;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int count;
|
||||||
|
TXFIELD slots[TX_MAX_SLOTS];
|
||||||
|
} TXBUF;
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 서비스 컨텍스트 (Tuxedo TPSVCINFO 대응) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
typedef struct {
|
||||||
|
char name[TX_KEY_LEN]; /* 호출된 서비스명 */
|
||||||
|
TXBUF *in; /* 요청 버퍼 */
|
||||||
|
TXBUF *out; /* 응답 버퍼 */
|
||||||
|
int rcode; /* tx_return 코드 */
|
||||||
|
long xid; /* 현재 트랜잭션 ID (0=없음) */
|
||||||
|
} TXSVCINFO;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TX_SERVICE(name, ctx) - 서비스 엔트리 정의 매크로
|
||||||
|
* (Tuxedo 의 void SVC(TPSVCINFO *) 시그니처를 흉내낸다)
|
||||||
|
*
|
||||||
|
* TX_SERVICE(AC_INTAKE, ctx) { ... tx_return(ctx, TX_OK, out); }
|
||||||
|
*/
|
||||||
|
typedef void (*tx_service_fn)(TXSVCINFO *ctx);
|
||||||
|
|
||||||
|
#define TX_SERVICE(name, ctx) void name(TXSVCINFO *ctx)
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 버퍼 API (Fchg/Fget 대응) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
TXBUF *tx_buf_alloc(void);
|
||||||
|
void tx_buf_free(TXBUF *buf);
|
||||||
|
void tx_buf_reset(TXBUF *buf);
|
||||||
|
|
||||||
|
/* 키에 문자열/바이너리 값을 설정 (없으면 추가, 있으면 갱신) */
|
||||||
|
int tx_buf_set(TXBUF *buf, const char *key, const char *val, int len);
|
||||||
|
/* 문자열 편의 setter (strlen 사용) */
|
||||||
|
int tx_buf_sets(TXBUF *buf, const char *key, const char *val);
|
||||||
|
/* long 값을 문자열로 저장 */
|
||||||
|
int tx_buf_setlong(TXBUF *buf, const char *key, long val);
|
||||||
|
|
||||||
|
/* 값을 out 으로 복사(널 종료). outlen 은 out 버퍼 크기 */
|
||||||
|
int tx_buf_get(TXBUF *buf, const char *key, char *out, int outlen);
|
||||||
|
/* long 으로 파싱하여 반환 */
|
||||||
|
int tx_buf_getlong(TXBUF *buf, const char *key, long *out);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 서비스 레지스트리 & 디스패처 (tpcall/tpreturn 대응) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
int tx_register(const char *name, tx_service_fn fn);
|
||||||
|
/* in -> 서비스 -> out. 로컬 레지스트리에서 해석되므로 단독 링크된다. */
|
||||||
|
int tx_call(const char *svc, TXBUF *in, TXBUF *out);
|
||||||
|
/* 서비스 내부에서 응답 반환 (out 을 ctx->out 으로 복사) */
|
||||||
|
int tx_return(TXSVCINFO *ctx, int rc, TXBUF *out);
|
||||||
|
/* 등록된 서비스 수 */
|
||||||
|
int tx_service_count(void);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 트랜잭션 API (XA tpbegin/tpcommit/tpabort 대응, 여기선 로깅 스텁) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
int tx_begin(void);
|
||||||
|
int tx_commit(void);
|
||||||
|
int tx_abort(void);
|
||||||
|
long tx_current_xid(void);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 로깅 (userlog 대응) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
void tx_log(int level, const char *fmt, ...);
|
||||||
|
void tx_set_loglevel(int level);
|
||||||
|
const char *tx_strerror(int rc);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* TXCORE_H */
|
||||||
331
legacy/framework/txcore/src/txcore.c
Normal file
331
legacy/framework/txcore/src/txcore.c
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
/*
|
||||||
|
* txcore.c - TxCore 공통 프레임워크 구현
|
||||||
|
*
|
||||||
|
* in-process 디스패치 테이블 + 고정 슬롯 키/값 버퍼로 TP 모니터 관용구를
|
||||||
|
* 재현한다. 외부 미들웨어 의존이 없으므로 gcc 만으로 링크된다.
|
||||||
|
*/
|
||||||
|
#include "txcore.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 전역 상태 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
#define TX_MAX_SERVICES 128
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char name[TX_KEY_LEN];
|
||||||
|
tx_service_fn fn;
|
||||||
|
} tx_entry_t;
|
||||||
|
|
||||||
|
static tx_entry_t g_registry[TX_MAX_SERVICES];
|
||||||
|
static int g_registry_count = 0;
|
||||||
|
|
||||||
|
static int g_loglevel = TX_LOG_INFO;
|
||||||
|
static long g_xid_seq = 0; /* 발급된 트랜잭션 ID 시퀀스 */
|
||||||
|
static long g_cur_xid = 0; /* 현재 활성 트랜잭션 */
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 로깅 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
static const char *level_name(int level)
|
||||||
|
{
|
||||||
|
switch (level) {
|
||||||
|
case TX_LOG_DEBUG: return "DEBUG";
|
||||||
|
case TX_LOG_INFO: return "INFO ";
|
||||||
|
case TX_LOG_WARN: return "WARN ";
|
||||||
|
case TX_LOG_ERROR: return "ERROR";
|
||||||
|
default: return "?????";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void tx_set_loglevel(int level)
|
||||||
|
{
|
||||||
|
g_loglevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tx_log(int level, const char *fmt, ...)
|
||||||
|
{
|
||||||
|
va_list ap;
|
||||||
|
time_t now;
|
||||||
|
struct tm tmv;
|
||||||
|
char ts[20];
|
||||||
|
|
||||||
|
if (level < g_loglevel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
now = time(NULL);
|
||||||
|
localtime_r(&now, &tmv);
|
||||||
|
strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tmv);
|
||||||
|
|
||||||
|
fprintf(stderr, "[%s] %s ", ts, level_name(level));
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vfprintf(stderr, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
fputc('\n', stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *tx_strerror(int rc)
|
||||||
|
{
|
||||||
|
switch (rc) {
|
||||||
|
case TX_OK: return "정상";
|
||||||
|
case TX_FAIL: return "일반 실패";
|
||||||
|
case TX_ENOENT: return "서비스 미등록";
|
||||||
|
case TX_EINVAL: return "파라미터 오류";
|
||||||
|
case TX_EDB: return "DB 오류";
|
||||||
|
case TX_ENOMEM: return "자원 부족";
|
||||||
|
case TX_ENOKEY: return "버퍼 키 없음";
|
||||||
|
default: return "알 수 없는 오류";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 버퍼 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
TXBUF *tx_buf_alloc(void)
|
||||||
|
{
|
||||||
|
TXBUF *b = (TXBUF *)calloc(1, sizeof(TXBUF));
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tx_buf_free(TXBUF *buf)
|
||||||
|
{
|
||||||
|
if (buf)
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tx_buf_reset(TXBUF *buf)
|
||||||
|
{
|
||||||
|
if (buf)
|
||||||
|
memset(buf, 0, sizeof(*buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
static TXFIELD *find_slot(TXBUF *buf, const char *key)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < TX_MAX_SLOTS; i++) {
|
||||||
|
if (buf->slots[i].used &&
|
||||||
|
strncmp(buf->slots[i].key, key, TX_KEY_LEN) == 0)
|
||||||
|
return &buf->slots[i];
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static TXFIELD *alloc_slot(TXBUF *buf, const char *key)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
TXFIELD *f = find_slot(buf, key);
|
||||||
|
if (f)
|
||||||
|
return f;
|
||||||
|
for (i = 0; i < TX_MAX_SLOTS; i++) {
|
||||||
|
if (!buf->slots[i].used) {
|
||||||
|
f = &buf->slots[i];
|
||||||
|
memset(f, 0, sizeof(*f));
|
||||||
|
strncpy(f->key, key, TX_KEY_LEN - 1);
|
||||||
|
f->used = 1;
|
||||||
|
buf->count++;
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL; /* 슬롯 부족 */
|
||||||
|
}
|
||||||
|
|
||||||
|
int tx_buf_set(TXBUF *buf, const char *key, const char *val, int len)
|
||||||
|
{
|
||||||
|
TXFIELD *f;
|
||||||
|
|
||||||
|
if (!buf || !key || !val)
|
||||||
|
return TX_EINVAL;
|
||||||
|
if (len < 0 || len >= TX_VAL_LEN)
|
||||||
|
return TX_ENOMEM;
|
||||||
|
|
||||||
|
f = alloc_slot(buf, key);
|
||||||
|
if (!f)
|
||||||
|
return TX_ENOMEM;
|
||||||
|
|
||||||
|
memcpy(f->val, val, len);
|
||||||
|
f->val[len] = '\0';
|
||||||
|
f->len = len;
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int tx_buf_sets(TXBUF *buf, const char *key, const char *val)
|
||||||
|
{
|
||||||
|
if (!val)
|
||||||
|
return TX_EINVAL;
|
||||||
|
return tx_buf_set(buf, key, val, (int)strlen(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
int tx_buf_setlong(TXBUF *buf, const char *key, long val)
|
||||||
|
{
|
||||||
|
char tmp[32];
|
||||||
|
snprintf(tmp, sizeof(tmp), "%ld", val);
|
||||||
|
return tx_buf_set(buf, key, tmp, (int)strlen(tmp));
|
||||||
|
}
|
||||||
|
|
||||||
|
int tx_buf_get(TXBUF *buf, const char *key, char *out, int outlen)
|
||||||
|
{
|
||||||
|
TXFIELD *f;
|
||||||
|
|
||||||
|
if (!buf || !key || !out || outlen <= 0)
|
||||||
|
return TX_EINVAL;
|
||||||
|
|
||||||
|
f = find_slot(buf, key);
|
||||||
|
if (!f)
|
||||||
|
return TX_ENOKEY;
|
||||||
|
|
||||||
|
if (f->len >= outlen)
|
||||||
|
return TX_ENOMEM;
|
||||||
|
|
||||||
|
memcpy(out, f->val, f->len);
|
||||||
|
out[f->len] = '\0';
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int tx_buf_getlong(TXBUF *buf, const char *key, long *out)
|
||||||
|
{
|
||||||
|
TXFIELD *f;
|
||||||
|
char *end;
|
||||||
|
long v;
|
||||||
|
|
||||||
|
if (!buf || !key || !out)
|
||||||
|
return TX_EINVAL;
|
||||||
|
|
||||||
|
f = find_slot(buf, key);
|
||||||
|
if (!f)
|
||||||
|
return TX_ENOKEY;
|
||||||
|
|
||||||
|
v = strtol(f->val, &end, 10);
|
||||||
|
if (end == f->val)
|
||||||
|
return TX_EINVAL;
|
||||||
|
|
||||||
|
*out = v;
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 레지스트리 & 디스패처 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
int tx_register(const char *name, tx_service_fn fn)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!name || !fn)
|
||||||
|
return TX_EINVAL;
|
||||||
|
if (g_registry_count >= TX_MAX_SERVICES)
|
||||||
|
return TX_ENOMEM;
|
||||||
|
|
||||||
|
/* 중복 등록 시 갱신 */
|
||||||
|
for (i = 0; i < g_registry_count; i++) {
|
||||||
|
if (strncmp(g_registry[i].name, name, TX_KEY_LEN) == 0) {
|
||||||
|
g_registry[i].fn = fn;
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strncpy(g_registry[g_registry_count].name, name, TX_KEY_LEN - 1);
|
||||||
|
g_registry[g_registry_count].fn = fn;
|
||||||
|
g_registry_count++;
|
||||||
|
tx_log(TX_LOG_DEBUG, "서비스 등록: %s", name);
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int tx_service_count(void)
|
||||||
|
{
|
||||||
|
return g_registry_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static tx_service_fn lookup(const char *name)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < g_registry_count; i++) {
|
||||||
|
if (strncmp(g_registry[i].name, name, TX_KEY_LEN) == 0)
|
||||||
|
return g_registry[i].fn;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int tx_call(const char *svc, TXBUF *in, TXBUF *out)
|
||||||
|
{
|
||||||
|
tx_service_fn fn;
|
||||||
|
TXSVCINFO ctx;
|
||||||
|
|
||||||
|
if (!svc || !in || !out)
|
||||||
|
return TX_EINVAL;
|
||||||
|
|
||||||
|
fn = lookup(svc);
|
||||||
|
if (!fn) {
|
||||||
|
tx_log(TX_LOG_ERROR, "tx_call: 서비스 미등록 svc=%s", svc);
|
||||||
|
return TX_ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&ctx, 0, sizeof(ctx));
|
||||||
|
strncpy(ctx.name, svc, TX_KEY_LEN - 1);
|
||||||
|
ctx.in = in;
|
||||||
|
ctx.out = out;
|
||||||
|
ctx.rcode = TX_OK;
|
||||||
|
ctx.xid = g_cur_xid;
|
||||||
|
|
||||||
|
tx_log(TX_LOG_DEBUG, "tx_call ▶ %s", svc);
|
||||||
|
fn(&ctx);
|
||||||
|
tx_log(TX_LOG_DEBUG, "tx_call ◀ %s rc=%d", svc, ctx.rcode);
|
||||||
|
|
||||||
|
return ctx.rcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
int tx_return(TXSVCINFO *ctx, int rc, TXBUF *out)
|
||||||
|
{
|
||||||
|
if (!ctx)
|
||||||
|
return TX_EINVAL;
|
||||||
|
|
||||||
|
ctx->rcode = rc;
|
||||||
|
/* 서비스가 별도 out 버퍼를 채웠다면 컨텍스트 응답 버퍼로 복사 */
|
||||||
|
if (out && ctx->out && out != ctx->out)
|
||||||
|
memcpy(ctx->out, out, sizeof(TXBUF));
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 트랜잭션 (XA 스텁: 실제 자원관리자 대신 로깅) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
int tx_begin(void)
|
||||||
|
{
|
||||||
|
if (g_cur_xid != 0) {
|
||||||
|
tx_log(TX_LOG_WARN, "tx_begin: 이미 진행중인 트랜잭션 xid=%ld", g_cur_xid);
|
||||||
|
return TX_FAIL;
|
||||||
|
}
|
||||||
|
g_cur_xid = ++g_xid_seq;
|
||||||
|
tx_log(TX_LOG_INFO, "tx_begin xid=%ld", g_cur_xid);
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int tx_commit(void)
|
||||||
|
{
|
||||||
|
if (g_cur_xid == 0) {
|
||||||
|
tx_log(TX_LOG_WARN, "tx_commit: 진행중인 트랜잭션 없음");
|
||||||
|
return TX_FAIL;
|
||||||
|
}
|
||||||
|
tx_log(TX_LOG_INFO, "tx_commit xid=%ld", g_cur_xid);
|
||||||
|
g_cur_xid = 0;
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int tx_abort(void)
|
||||||
|
{
|
||||||
|
if (g_cur_xid == 0) {
|
||||||
|
tx_log(TX_LOG_WARN, "tx_abort: 진행중인 트랜잭션 없음");
|
||||||
|
return TX_FAIL;
|
||||||
|
}
|
||||||
|
tx_log(TX_LOG_WARN, "tx_abort xid=%ld", g_cur_xid);
|
||||||
|
g_cur_xid = 0;
|
||||||
|
return TX_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
long tx_current_xid(void)
|
||||||
|
{
|
||||||
|
return g_cur_xid;
|
||||||
|
}
|
||||||
Reference in a new issue