commit ff0b0b60a22a75a955455ae0a4cc2f30be1d5b66 Author: forge-bot Date: Sat Jul 18 09:55:46 2026 +0000 chore: acquire-core migration monorepo (legacy C + Spring Boot target skeleton) TxCore/ECPG legacy 매입·정산 C 슬라이스(빌드검증) + Spring Boot 멀티모듈 골격(mvn test green) + MIGRATION.md 전환룰 + CI(boot 빌드). diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..6825bcd --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..523fd12 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..4529897 --- /dev/null +++ b/MIGRATION.md @@ -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` | 정형 필드는 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.` + `boot/modules//` + +## 2단계 의존 순서 (반드시 준수) + +1. **1단계 — 공통 프레임워크 우선.** TxCore 대체(`common-framework`)가 먼저 + 착지해야 한다. 업무 모듈이 의존하는 `TxContext` / `MessageCodec` / 트랜잭션·로깅 + 규약이 여기 있다. 프레임워크 없이 업무 모듈만 이식하면 빌드가 깨진다. +2. **2단계 — 업무 모듈.** `modules/acquiring` 은 `common-framework` 에 의존한다 + (`pom.xml` ``). 매입접수 → 정산집계 → 대사 배치 순으로 유스케이스를 + 채운다. + +## 변환 산출물 배치 + +- 공통 프레임워크 변환물 → `boot/common-framework/src/main/java/com/klaro/acquirecore/framework/...` +- 업무 모듈 변환물 → `boot/modules//src/main/java/com/klaro/acquirecore//...` +- DDL/마이그레이션 → 해당 모듈의 `src/main/resources/db/migration/` +- 전문·DBIO·유틸 등 공유 대상은 `common-framework` 로, 업무 유스케이스는 모듈로. + +## 원칙 + +- `legacy/` 는 **읽기 전용 소스**다. 에이전트는 수정하지 않는다. +- 각 변환은 컴파일 + `mvn test` 초록을 유지한 채 증분으로 진행한다. +- 스켈레톤 단계에서는 데이터소스를 배선하지 않는다(스모크 테스트는 Postgres 불필요). + DBIO 이식 시점에 Flyway + datasource 를 추가한다. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f18c5a --- /dev/null +++ b/README.md @@ -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). diff --git a/boot/common-framework/pom.xml b/boot/common-framework/pom.xml new file mode 100644 index 0000000..9629ef0 --- /dev/null +++ b/boot/common-framework/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + com.klaro.acquirecore + acquire-core-boot + 0.0.1-SNAPSHOT + ../pom.xml + + + common-framework + jar + common-framework + TxCore 의 Spring 대체 공통 프레임워크 스타터 (라이브러리 jar) + + + + + org.springframework.boot + spring-boot-starter + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/MessageCodec.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/MessageCodec.java new file mode 100644 index 0000000..f3b84de --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/MessageCodec.java @@ -0,0 +1,24 @@ +package com.klaro.acquirecore.framework; + +import org.springframework.stereotype.Component; + +/** + * 레거시 {@code msg_layout.h} 고정길이 전문(電文) 코덱의 Spring 빈 자리표시자. + * + *

실 전환에서는 고정길이(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); + } +} diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxContext.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxContext.java new file mode 100644 index 0000000..1a56634 --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxContext.java @@ -0,0 +1,31 @@ +package com.klaro.acquirecore.framework; + +import java.util.HashMap; +import java.util.Map; + +/** + * 레거시 TxCore {@code TXBUF} (고정 슬롯 키/값 버퍼) 의 Spring 대체 자리표시자. + * + *

실 전환에서는 요청/응답 DTO(record) 가 정형 필드를 담고, 이 컨텍스트는 + * 서비스 체인({@code tx_call}) 간 전달되는 느슨한 키/값 상태만 보관한다. + */ +public final class TxContext { + + private final Map 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(); + } +} diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/package-info.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/package-info.java new file mode 100644 index 0000000..4f3260c --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/package-info.java @@ -0,0 +1,12 @@ +/** + * TxCore 공통 프레임워크의 Spring 대체 계층. + * + *

레거시 {@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; diff --git a/boot/modules/acquiring/pom.xml b/boot/modules/acquiring/pom.xml new file mode 100644 index 0000000..918ad86 --- /dev/null +++ b/boot/modules/acquiring/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + + com.klaro.acquirecore + acquire-core-boot + 0.0.1-SNAPSHOT + ../../pom.xml + + + acquiring + jar + acquiring + 매입(acquiring) 업무 모듈 — common-framework 에 의존하는 Spring Boot 앱 + + + + + com.klaro.acquirecore + common-framework + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/AcquiringApplication.java b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/AcquiringApplication.java new file mode 100644 index 0000000..5524eff --- /dev/null +++ b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/AcquiringApplication.java @@ -0,0 +1,22 @@ +package com.klaro.acquirecore.acquiring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 매입(acquiring) 업무 모듈 부트스트랩. + * + *

레거시 {@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); + } +} diff --git a/boot/modules/acquiring/src/main/resources/application.yml b/boot/modules/acquiring/src/main/resources/application.yml new file mode 100644 index 0000000..78b3259 --- /dev/null +++ b/boot/modules/acquiring/src/main/resources/application.yml @@ -0,0 +1,9 @@ +spring: + application: + name: acquiring + # 스켈레톤 단계에서는 데이터소스를 배선하지 않는다(스모크 테스트가 Postgres 불필요). + # 매입 DBIO(purchase_dbio.pgc → MyBatis/Spring Data) 이식 시 datasource 를 추가한다. + +logging: + level: + com.klaro.acquirecore: INFO diff --git a/boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/SmokeTest.java b/boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/SmokeTest.java new file mode 100644 index 0000000..eb1995a --- /dev/null +++ b/boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/SmokeTest.java @@ -0,0 +1,20 @@ +package com.klaro.acquirecore.acquiring; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * 스켈레톤 스모크 테스트. + * + *

DB/컨텍스트 없이 순수 JUnit 으로 빌드 파이프라인(컴파일 + `mvn test`)이 + * 초록인지만 증명한다. common-framework 이식이 진행되면 실제 유스케이스 + * 테스트로 대체된다. + */ +class SmokeTest { + + @Test + void skeletonBuilds() { + assertTrue(true, "Spring Boot 매입 모듈 스켈레톤이 빌드된다"); + } +} diff --git a/boot/pom.xml b/boot/pom.xml new file mode 100644 index 0000000..110aa03 --- /dev/null +++ b/boot/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.5 + + + + com.klaro.acquirecore + acquire-core-boot + 0.0.1-SNAPSHOT + pom + acquire-core-boot + Spring Boot 전환 타겟: TxCore 공통 프레임워크 + 매입 업무 모듈 + + + 17 + 17 + UTF-8 + + + + common-framework + modules/acquiring + + + + + + + com.klaro.acquirecore + common-framework + ${project.version} + + + + + diff --git a/legacy/.gitignore b/legacy/.gitignore new file mode 100644 index 0000000..5d58427 --- /dev/null +++ b/legacy/.gitignore @@ -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 diff --git a/legacy/Makefile b/legacy/Makefile new file mode 100644 index 0000000..9d61a47 --- /dev/null +++ b/legacy/Makefile @@ -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) diff --git a/legacy/README.md b/legacy/README.md new file mode 100644 index 0000000..7adca94 --- /dev/null +++ b/legacy/README.md @@ -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 [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` 컨텍스트 | +| `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 마이그레이션 | +``` diff --git a/legacy/app/batch/rc_match_batch.pgc b/legacy/app/batch/rc_match_batch.pgc new file mode 100644 index 0000000..85c01ea --- /dev/null +++ b/legacy/app/batch/rc_match_batch.pgc @@ -0,0 +1,147 @@ +/* + * rc_match_batch.pgc - 승인-매입 대사(對査) 배치 (RC_MATCH) + * + * 지정 영업일의 접수(status='R') 매입 건을 커서로 순회하며 승인원장과 + * 대사한다. 금액이 일치하면 대사완료('M'), 아니면 불일치('U') 로 갱신하고, + * 처리 요약을 출력한다. 배치 main 엔트리를 포함한다. + * + * 실행: rc_match_batch [db@host] + */ +#include +#include +#include + +#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 [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; +} diff --git a/legacy/app/common/util_amount.c b/legacy/app/common/util_amount.c new file mode 100644 index 0000000..94b0549 --- /dev/null +++ b/legacy/app/common/util_amount.c @@ -0,0 +1,82 @@ +/* + * util_amount.c - 금액/통화 유틸리티 (plain C) + */ +#include "acq_util.h" + +#include +#include +#include + +/* 거래금액 상한 (원). 건당 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; +} diff --git a/legacy/app/common/util_date.c b/legacy/app/common/util_date.c new file mode 100644 index 0000000..70cbed6 --- /dev/null +++ b/legacy/app/common/util_date.c @@ -0,0 +1,135 @@ +/* + * util_date.c - 일자/영업일 유틸리티 (plain C) + */ +#include "acq_util.h" + +#include +#include +#include +#include + +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); +} diff --git a/legacy/app/common/util_msg.c b/legacy/app/common/util_msg.c new file mode 100644 index 0000000..92ee1cd --- /dev/null +++ b/legacy/app/common/util_msg.c @@ -0,0 +1,109 @@ +/* + * util_msg.c - 고정길이 전문 pack/unpack 유틸리티 (plain C) + */ +#include "acq_util.h" + +#include +#include + +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; +} diff --git a/legacy/app/dbio/purchase_dbio.pgc b/legacy/app/dbio/purchase_dbio.pgc new file mode 100644 index 0000000..4b8a2c7 --- /dev/null +++ b/legacy/app/dbio/purchase_dbio.pgc @@ -0,0 +1,206 @@ +/* + * purchase_dbio.pgc - 매입/정산 DBIO (ECPG 임베디드 SQL) + * + * Pro*C 상당의 PostgreSQL ECPG 로 매입/승인/정산 테이블에 대한 + * INSERT/SELECT/UPDATE 를 캡슐화한다. 서비스/배치는 이 함수만 호출. + */ +#include +#include + +#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; +} diff --git a/legacy/app/include/acq_util.h b/legacy/app/include/acq_util.h new file mode 100644 index 0000000..3ee8742 --- /dev/null +++ b/legacy/app/include/acq_util.h @@ -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 */ diff --git a/legacy/app/include/msg_layout.h b/legacy/app/include/msg_layout.h new file mode 100644 index 0000000..76087c3 --- /dev/null +++ b/legacy/app/include/msg_layout.h @@ -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 */ diff --git a/legacy/app/include/purchase_dbio.h b/legacy/app/include/purchase_dbio.h new file mode 100644 index 0000000..605a958 --- /dev/null +++ b/legacy/app/include/purchase_dbio.h @@ -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 */ diff --git a/legacy/app/online/ac_intake_svc.pgc b/legacy/app/online/ac_intake_svc.pgc new file mode 100644 index 0000000..6f088d0 --- /dev/null +++ b/legacy/app/online/ac_intake_svc.pgc @@ -0,0 +1,136 @@ +/* + * ac_intake_svc.pgc - 매입접수 서비스 (AC_INTAKE) + * + * MG_RECV 가 적재한 TXBUF 필드를 검증하고, 트랜잭션 하에서 + * purchase 테이블에 접수 INSERT 한 뒤 정산집계 서비스를 tx_call 한다. + */ +#include +#include + +#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); +} diff --git a/legacy/app/online/mg_recv_svc.pgc b/legacy/app/online/mg_recv_svc.pgc new file mode 100644 index 0000000..71cfa7b --- /dev/null +++ b/legacy/app/online/mg_recv_svc.pgc @@ -0,0 +1,71 @@ +/* + * mg_recv_svc.pgc - 전문수신 서비스 (MG_RECV) + * + * 대외 매입요청 전문(고정길이 100바이트)을 수신하여 unpack 하고, + * 필드를 TXBUF 로 적재한 뒤 후속 서비스가 소비하도록 응답한다. + * (ATMI 의 void SVC(TPSVCINFO*) 대응) + */ +#include +#include + +#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); +} diff --git a/legacy/app/online/server_main.c b/legacy/app/online/server_main.c new file mode 100644 index 0000000..597c65a --- /dev/null +++ b/legacy/app/online/server_main.c @@ -0,0 +1,36 @@ +/* + * server_main.c - acquire-core 온라인 서버 데모 부트스트랩 + * + * Tuxedo 의 tmboot/서버 메인처럼, 온라인 서비스들을 TxCore 레지스트리에 + * 등록한다. 실제 TP 모니터에서는 tpsvrinit() 에 해당한다. 여기서는 + * 등록/디스패치 링크 경로를 검증하기 위한 최소 데모 엔트리다. + */ +#include + +#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; +} diff --git a/legacy/db/schema.sql b/legacy/db/schema.sql new file mode 100644 index 0000000..41fae81 --- /dev/null +++ b/legacy/db/schema.sql @@ -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) +); diff --git a/legacy/framework/txcore/dbio/txcore_dbio.h b/legacy/framework/txcore/dbio/txcore_dbio.h new file mode 100644 index 0000000..fbbb22f --- /dev/null +++ b/legacy/framework/txcore/dbio/txcore_dbio.h @@ -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 */ diff --git a/legacy/framework/txcore/include/txcore.h b/legacy/framework/txcore/include/txcore.h new file mode 100644 index 0000000..affb962 --- /dev/null +++ b/legacy/framework/txcore/include/txcore.h @@ -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 +#include + +#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 */ diff --git a/legacy/framework/txcore/src/txcore.c b/legacy/framework/txcore/src/txcore.c new file mode 100644 index 0000000..8a52222 --- /dev/null +++ b/legacy/framework/txcore/src/txcore.c @@ -0,0 +1,331 @@ +/* + * txcore.c - TxCore 공통 프레임워크 구현 + * + * in-process 디스패치 테이블 + 고정 슬롯 키/값 버퍼로 TP 모니터 관용구를 + * 재현한다. 외부 미들웨어 의존이 없으므로 gcc 만으로 링크된다. + */ +#include "txcore.h" + +#include +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* 전역 상태 */ +/* ------------------------------------------------------------------ */ +#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; +}