블루라이언스
경력근거: 외부 API 연동 (금융정보 API, 결제 PG사, Firebase 푸시알림)
※ 실제 산출물이 아닌 AI 예측 시안입니다.
공고는 필수로 Spring Boot 와 NestJS 백엔드를 함께 두고, 우대가 아니라 주요업무에 MySQL 쿼리 최적화(QueryDSL·TypeORM), 웹 스크래핑 API, 금융정보 API 와 결제 PG사 연동, Firebase 푸시를 한 줄씩 나열합니다. 이 포트폴리오는 그 목록을 장식이 아니라 골조로 씁니다. 정산 파일을 MySQL 로 정규화하고 Spring Boot 가 대조 규칙을, NestJS 가 업로드와 조회를 맡으며 어긋난 건이 생기면 Firebase 로 알립니다. 다만 이 포트폴리오는 백엔드 설계와 대조 규칙 구현을 보여줍니다. 카드사 정산 실무나 PG 가맹점 심사 규정에 대한 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
공고는 Spring Boot 기반 RESTful API 서버와 NestJS 기반 B2B 서비스 백엔드를 한 사람의 업무로 나란히 둡니다. 서비스가 둘로 갈려 있으니 기능 하나를 붙일 때 어느 쪽에 둘지 정하는 판단이 그대로 업무가 됩니다.
웹 스크래핑 API 개발과 금융정보 API·결제 PG사·Firebase 푸시 연동이 주요업무에 함께 적혀 있습니다. 사내에서 만든 데이터보다 밖에서 받아 온 데이터를 맞춰 넣는 쪽이 일의 무게중심입니다.
MySQL 데이터베이스 설계와 쿼리 최적화를 요구하면서 QueryDSL·TypeORM 을 나란히 적습니다. 테이블을 만들고 끝내는 자리가 아니라 조회가 느려지는 지점을 직접 잡는 자리입니다.
정산 담당자가 자사 주문 기록과 PG사에서 받은 정산 파일을 함께 올리면, 두 쪽을 주문번호로 맞춰 어긋난 건만 사유별로 모아 줍니다. 어긋났다는 판정은 금액·수수료·상태를 비교하는 규칙이 정하고, 모델은 판정에 관여하지 않고 어느 값이 얼마나 벌어졌는지를 사람이 읽을 한 줄로 요약합니다.
저는 B2B 서비스의 정산을 맡고 있습니다. 매달 초 PG사 정산 파일을 내려받아 우리 주문 테이블과 맞춰 보는데, 주문번호 표기가 조금씩 달라 엑셀 함수로는 절반쯤밖에 안 붙습니다. 안 맞는 건이 나와도 금액이 틀린 건지 수수료가 틀린 건지, 아니면 아예 우리 쪽에 없는 주문인지 하나씩 열어 봐야 압니다.
정산 대조가 오래 걸리는 이유는 계산이 어려워서가 아니라 어긋난 이유가 분류되지 않은 채 목록으로만 쌓이기 때문입니다. 표기 흔들림을 정규화해 붙이고 어긋난 사유를 여섯 가지로 나눠 근거와 같이 보여주면, 담당자가 사유별로 처리하고 넘어갑니다.
정산 담당자가 쓰는 화면 하나가 돕니다. 주문 기록과 정산 파일을 올리면 주문번호를 정규화해 맞추고, 어긋난 건을 사유별로 근거와 같이 보여줍니다. 파일은 직접 올리는 것으로 시작하고, PG 관리자 화면을 긁어 오는 자동 수집은 3주차로 미룹니다.
정답을 붙인 대조 60건으로 사유 분류가 일치하는지 자동으로 맞춰 보고, 어긋난 유형을 리포트로 냅니다. 2주차부터 AWS 로 배포해 담당자가 사내 주소로 들어와 씁니다.
PG 관리자 화면에서 정산 파일을 내려받는 수집기를 붙여 사람이 올리지 않아도 대조가 돌고, 어긋난 건이 생기면 Firebase 푸시로 담당자에게 알립니다.
정산 담당자가 파일 두 장을 올리면 표기가 다른 주문번호도 붙고, 어긋난 건을 사유별로 근거와 같이 보여줍니다.
열 이름이 빠지거나 금액 칸이 비거나 인코딩이 깨진 파일 12종을 올려도 화면이 죽지 않고, 어느 줄이 왜 걸렸는지 담당자가 그 자리에서 읽습니다.
수수료 미입력과 전액 환불로 0원이 된 건, 자사에 없는 주문번호, 띄어쓰기가 흔들린 표기를 섞은 입력 30종을 넣어도 응답 형식이 깨지지 않고, 못 붙인 건은 미매핑으로 보여줍니다.
정답을 붙인 대조 60건으로 사유 분류 일치율을 냅니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이며 실제 대조 적중률은 블루라이언스의 실 주문 데이터와 PG사 정산 파일이 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 결측·경계값·미등록 주문번호·표기 흔들림을 더미에 일부러 섞어 쉬운 문제만 푸는 함정을 피합니다.
금액이 다르다는 사실이 곧 오류를 뜻하지는 않습니다. 부분 환불이나 월 경계 이월로도 벌어지므로 사유 판정을 결론으로 읽지 않게 근거를 항상 같이 보여줍니다.
실제 도입에서는 PG사마다 정산 파일의 열 이름과 수수료 계산 방식이 다릅니다. 사별 매핑표가 늘어나면 규칙을 코드에서 빼 설정으로 옮겨야 하며, 그것은 도입 단계 과제로 분리합니다.
결제 실행과 정산 이체는 가맹점 계약이 있어야 다룰 수 있어 이 포트폴리오의 범위 밖입니다. 여기서는 이미 끝난 거래의 기록을 맞춰 보는 데까지만 다룹니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
자사 서비스에 남은 주문 기록입니다. 대조의 한쪽 축이며 수수료 미입력과 표기가 흔들리는 주문번호를 의도적으로 포함합니다.
| order_id | ordered_at | merchant_order_no_raw | amount_krw | fee_krw | pay_method | status | memo |
|---|---|---|---|---|---|---|---|
| ord_20001 | 2026-07-01T10:12:00 | BL-2026-0001 | 128,000 | 3,840 | card | paid | — |
| ord_20002 | 2026-07-01T14:40:00 | bl 2026 0002 | 55,000 | 1,650 | card | paid | 표기 흔들림 |
| ord_20003 | 2026-07-02T09:05:00 | BL-2026-0003 | 240,000 | — | transfer | paid | 수수료 미입력 |
| ord_20004 | 2026-07-02T18:22:00 | BL-2026-0004 | 0 | 0 | card | cancelled | 전액 환불 |
| ord_20005 | 2026-07-03T11:31:00 | BL-2026-0005 | 99,000 | 2,970 | virtual_account | paid | — |
아래 JSON Schema 에 맞는 주문 행을 더 만들어 주세요. {"type":"object","properties":{"order_id":{"type":"string"},"ordered_at":{"type":"string"},"merchant_order_no_raw":{"type":"string"},"amount_krw":{"type":"integer"},"fee_krw":{"type":["integer","null"]},"pay_method":{"type":"string","enum":["card","transfer","virtual_account","point"]},"status":{"type":"string","enum":["paid","cancelled","partial_refund"]},"memo":{"type":["string","null"]}},"required":["order_id","ordered_at","merchant_order_no_raw","amount_krw","pay_method","status"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) fee_krw 가 null 인 수수료 미입력 건, (2) amount_krw 가 0 인 전액 환불 건과 수백만원대 최대 건, (3) merchant_order_no_raw 의 구분자가 하이픈·언더바·공백으로 흔들리고 대소문자가 섞인 건, (4) 정산 파일에는 없는 주문번호. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"order_id":{"type":"string"},"ordered_at":{"type":"string"},"merchant_order_no_raw":{"type":"string"},"amount_krw":{"type":"integer"},"fee_krw":{"type":["integer","null"]},"pay_method":{"type":"string","enum":["card","transfer","virtual_account","point"]},"status":{"type":"string","enum":["paid","cancelled","partial_refund"]},"memo":{"type":["string","null"]}},"required":["order_id","ordered_at","merchant_order_no_raw","amount_krw","pay_method","status"]}PG사에서 받은 정산 내역입니다. 대조의 다른 쪽 축이며 자사에 없는 주문번호와 상태가 어긋난 건을 포함합니다.
| settle_id | settled_on | merchant_order_no_raw | order_id | settled_amount_krw | fee_krw | pg_name | settle_status | note |
|---|---|---|---|---|---|---|---|---|
| stl_90001 | 2026-07-11 | BL-2026-0001 | ord_20001 | 128,000 | 3,840 | 케이지이니시스 | paid | — |
| stl_90002 | 2026-07-11 | BL-2026-0002 | — | 55,000 | 1,650 | 케이지이니시스 | paid | 표기가 달라 자동 매칭 실패 |
| stl_90003 | 2026-07-11 | BL-2026-0003 | ord_20003 | 240,000 | 7,200 | 토스페이먼츠 | paid | 주문 쪽 수수료 미입력 |
| stl_90004 | 2026-07-11 | BL-2026-0004 | ord_20004 | 0 | 0 | 케이지이니시스 | cancelled | — |
| stl_90005 | 2026-07-12 | BL-2026-0005 | ord_20005 | 98,000 | 2,940 | 나이스페이 | paid | 금액 1000원 차이 |
아래 JSON Schema 에 맞는 정산 행을 더 만들어 주세요. {"type":"object","properties":{"settle_id":{"type":"string"},"settled_on":{"type":"string"},"merchant_order_no_raw":{"type":"string"},"order_id":{"type":["string","null"]},"settled_amount_krw":{"type":"integer"},"fee_krw":{"type":["integer","null"]},"pg_name":{"type":"string"},"settle_status":{"type":"string","enum":["paid","cancelled","hold"]},"note":{"type":["string","null"]}},"required":["settle_id","settled_on","merchant_order_no_raw","settled_amount_krw","pg_name","settle_status"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) order_id 가 null 이라 번호로만 붙여야 하는 건, (2) 자사 주문에 아예 없는 merchant_order_no_raw, (3) 주문은 취소인데 정산은 지급으로 남은 상태 충돌 건, (4) fee_krw 가 null 인 보류 건과 금액이 수백원 단위로 어긋난 건. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"settle_id":{"type":"string"},"settled_on":{"type":"string"},"merchant_order_no_raw":{"type":"string"},"order_id":{"type":["string","null"]},"settled_amount_krw":{"type":"integer"},"fee_krw":{"type":["integer","null"]},"pg_name":{"type":"string"},"settle_status":{"type":"string","enum":["paid","cancelled","hold"]},"note":{"type":["string","null"]}},"required":["settle_id","settled_on","merchant_order_no_raw","settled_amount_krw","pg_name","settle_status"]}| Java | 대조 엔진 본체의 언어입니다. 금액·수수료·상태를 비교해 사유 코드를 정하는 규칙이 여기 있습니다. |
| Spring Boot | 대조 실행과 결과 조회를 맡는 RESTful API 서버입니다. 규칙 실행은 이쪽 한 곳에만 둡니다. |
| NestJS | 파일 업로드와 화면용 조회를 맡는 B2B 어드민 백엔드입니다. 대조 엔진을 직접 부르고 결과만 화면에 넘깁니다. |
| React | 정산 담당자가 실제로 보는 화면입니다. 사유별 묶음과 근거를 한 화면에 둡니다. |
| MySQL | 주문과 정산 내역을 담는 저장소입니다. 원문 주문번호와 정규화 키를 분리한 스키마가 대조 적중률을 좌우합니다. |
| QueryDSL | 사유·기간·PG사로 거르는 조회 쿼리를 조립합니다. 건수가 늘어도 인덱스를 타게 조건을 붙입니다. |
| TypeORM | 어드민 쪽 업로드 이력과 사용자 테이블을 다룹니다. |
| Selenium | 3주차에 PG 관리자 화면에 로그인해 정산 파일을 내려받는 수집기입니다. |
| Firebase | 어긋난 건이 새로 생기면 담당자에게 푸시로 알립니다. |
| AWS | 2주차부터 배포·운영을 맡습니다. 대조 리포트와 업로드 원본을 S3 에 남기고 RDS 로 MySQL 을 띄웁니다. |
| LLM | 사유 코드가 정해진 뒤 그 이유를 사람이 읽을 한 줄로 요약합니다. 어긋났는지 아닌지는 규칙이 정합니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 사유 분류가 실제로 맞았는지 정답 세트와 맞춰 보는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 정산 파일 수집을 자동화하고 어긋난 건을 푸시로 알리는 단계로 확장합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
reason_code사유 라벨 · merchant_order_no + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
amount_gap금액 차이fee_gap수수료 차이missing_in_orders정산에만 존재missing_in_settlement주문에만 존재status_conflict상태 불일치unmapped_no주문번호 미매핑대조에서 걸린 한 건을 사유 코드와 근거 행으로 남기는 형식입니다. 사유는 규칙이 정하고 요약 문장만 모델이 씁니다.
{"type":"object","properties":{"finding_id":{"type":"string"},"merchant_order_no":{"type":"string"},"reason_code":{"type":"string","enum":["amount_gap","fee_gap","missing_in_orders","missing_in_settlement","status_conflict","unmapped_no"]},"gap_krw":{"type":["integer","null"]},"evidence":{"type":"array","items":{"type":"object","properties":{"side":{"type":"string","enum":["order","settlement"]},"field":{"type":"string"},"value":{"type":["string","integer","null"]}},"required":["side","field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["finding_id","merchant_order_no","reason_code","evidence","summary"]}[
{
"finding_id": "fnd_0001",
"merchant_order_no": "BL-2026-0005",
"reason_code": "amount_gap",
"gap_krw": 1000,
"evidence": [
{
"side": "order",
"field": "amount_krw",
"value": 99000
},
{
"side": "settlement",
"field": "settled_amount_krw",
"value": 98000
}
],
"summary": "주문 금액보다 정산 금액이 1,000원 적습니다.",
"hedge": null
},
{
"finding_id": "fnd_0002",
"merchant_order_no": "BL-2026-0099",
"reason_code": "missing_in_orders",
"gap_krw": null,
"evidence": [
{
"side": "settlement",
"field": "merchant_order_no_raw",
"value": "BL-2026-0099"
},
{
"side": "order",
"field": "merchant_order_no_raw",
"value": null
}
],
"summary": "정산 파일에만 있는 주문번호라 자사 주문에서 찾지 못했습니다.",
"hedge": "합성 기준 판정이라 실제로는 취소 후 재발행 같은 다른 사유일 수 있습니다."
},
{
"finding_id": "fnd_0003",
"merchant_order_no": "BL-2026-0011",
"reason_code": "status_conflict",
"gap_krw": null,
"evidence": [
{
"side": "order",
"field": "status",
"value": "cancelled"
},
{
"side": "settlement",
"field": "settle_status",
"value": "paid"
},
{
"side": "order",
"field": "amount_krw",
"value": 150000
},
{
"side": "settlement",
"field": "settled_amount_krw",
"value": 150000
}
],
"summary": "주문은 취소인데 정산에는 지급으로 남아 있습니다. 금액은 양쪽이 같아 차이로 볼 수 없어 비웠습니다.",
"hedge": null
},
{
"finding_id": "fnd_0004",
"merchant_order_no": "BL-2026-0003",
"reason_code": "fee_gap",
"gap_krw": null,
"evidence": [
{
"side": "order",
"field": "fee_krw",
"value": null
},
{
"side": "settlement",
"field": "fee_krw",
"value": 7200
}
],
"summary": "주문 쪽 수수료가 비어 있어 정산 수수료와 맞춰 볼 수 없습니다.",
"hedge": null
}
][자동 검증 — 코드가 판정합니다] 1. reason_code 가 enum 여섯 값 중 하나인지 확인합니다. 규칙이 정한 코드라 모델이 새 코드를 만들면 그 응답을 버립니다. 2. evidence 의 field 이름이 실제 입력 행에 있는 컬럼인지 대조합니다. 없는 이름을 인용하면 요약이 지어낸 문장이라는 뜻이라 규칙이 만든 기본 문장으로 되돌립니다. 3. gap_krw 가 두 근거 값의 차이와 일치하는지 다시 계산합니다. 4. summary 가 존댓말 한 문장인지, 수치가 근거 행의 값과 같은지 확인합니다. [수동 검증 — 사람이 봅니다] 1. 사유 코드가 사람이 보기에도 그 코드가 맞는지 표본 20건을 눈으로 확인합니다. 2. 부분 환불처럼 한 건이 두 사유에 걸릴 때 어느 쪽으로 묶는 것이 담당자에게 편한지 판단합니다.