# #4220 추가 12건 개발계 실행 기록

- 실행: 2026-10-13 (Asia/Seoul), ADMIN, TypeScript/Playwright 헤드리스 브라우저와 인증된 API 요청
- 대상: `admin-dev.biscuitpay.com`, `api-dev.biscuitpay.com`, 테넌트 `11f0-5c83-7e840e98-b583-000000000001`
- 최종 판정: **PASS 11 / FAIL 0 / 개인확인 1**. ADD-10은 사용자 변경 기준을 기존 배포 후 실측 결과에 적용해 PASS로 정정했다. 최초 배포의 중복 저장 실패 이력은 아래에 보존한다. 기존 보고서의 과거 PASS와 합산하지 않는다.
- 원본: 비공개 `qa-results/issues/4220/dev/additional-baseline.json`; 실행 응답은 같은 디렉터리의 `additional-execution.json`, `additional-ui-execution.json`, `additional-ui-cases.json`에 저장. 연동정보 원문은 이 문서에 싣지 않는다.

| ID | 결과 | 개발계 실행 증거 |
| --- | --- | --- |
| ADD-01 | PASS | PG 계약 화면에서 두 수기 MID 값을 한 요청으로 교환. PATCH 200, 화면 응답 1,797ms. API 재조회로 두 값과 계약 확인. |
| ADD-02 | PASS | 미선택 MID와 동일값 요청 PATCH 400 `변경하려는 계약에 동일한 MID가 존재합니다.`. 선택 2건과 미선택 1건 모두 원본 유지. |
| ADD-03 | PASS | 선택한 두 MID의 같은 최종값 PATCH 400 `변경 대상에 동일한 MID 값이 포함되어 있습니다.`. 두 건 모두 원본 유지. |
| ADD-04 | PASS | 다른 계약의 수기 MID 두 건을 동일값으로 각각 변경. 두 요청 모두 200, 계약은 각자 유지. |
| ADD-05 | **PASS (기대값 정정)** | 변경 대상 `100410059-test`는 **수기(KEYIN)**, 다른 계약·법인의 `lucy-dan`은 **단말기(TERMINAL)**. 수기 MID에 그 값을 입력하자 API 및 PG 계약 화면 경로 모두 PATCH 200. 저장 직후 유형·계약·값을 재조회했다. 현행 유형별 중복 규칙에 맞으며, 기존 QA의 거절 기대가 잘못됐다. |
| ADD-06 | PASS | 루시페이먼츠 MID 1건을 웰컴페이먼츠 계약으로 이동, 기존 웰컴 MID 1건 값 수정, 선택된 나머지 1건 무변경. 한 PATCH 200 후 각 상세 재조회. |
| ADD-07 | PASS | 두 MID 중 한 건의 필수 IV를 비우자 화면에서 `MID별 PG 연동정보의 필수 항목을 확인해주세요.`가 보이고 PATCH 0건. 별도 서버 요청은 400 `웰컴페이먼츠 PG는 IV(iv) 설정 값이 필수입니다`; 정상 입력 건까지 모두 원본 유지. |
| ADD-08 | PASS | 계약 유지 상태에서 API KEY만 화면으로 두 차례 수정·저장(각 PATCH 200). IV 입력은 비워 두었고 저장값은 유지. 매번 상세 재조회 후 원본값 복원. |
| ADD-09 | **개인확인** | 연결 사용자 1명인 수기 MID를 펌뱅킹 정보가 있는 계약으로 이동해 200, 연결 사용자 유지 확인. 연결 가맹점이 있는 단말기 MID를 펌뱅킹 정보가 없는 계약으로 이동한 요청도 200. 서버 규칙은 연결 부서가 정산 시스템 이관 비활성일 때만 펌뱅킹 정보를 요구한다. 해당 조건의 연결 부서·계약 조합은 담당자 개인확인 대상으로 남겼다. 200만으로 결함 판정하지 않는다. |
| ADD-10 | **PASS (배포 후 재QA)** | 최초 배포에서는 같은 계약의 수기(KEYIN) MID 두 건이 동시 저장 200/200으로 중복되어 실패했다. 수정 배포 후 독립 브라우저 경합 6회는 모두 200/500, 최종 동일값 1건, 실패 건 원본 상태 유지. 사용자가 변경한 판정 기준에 따라 동시성 충돌 요청이 실패 처리되어 무결성이 보호된 것으로 판정했다. 500 일반 오류 문구 관측은 그대로 남긴다. |
| ADD-11 | PASS | TID/MID 관리의 최신 `MID 변경` 모달에서 저장 200. TID/MID 관리 재검색과 PG 계약 관리 새로고침에서 같은 MID 값·계약을 확인. 대상 계약 목록에 현재 계약도 표시됨. |
| ADD-12 | PASS | MID 2,006건이 있는 기존 계약에서 1건만 화면으로 변경. PATCH 응답까지 500ms, 재검색 완료까지 3,269ms. 선택 건은 변경, 인접 기존 MID는 유지. API 단독 요청은 172ms. DB 부하·장기 성능을 이 수치만으로 판정하지 않는다. |

## 중복 규칙과 실패 분석

**ADD-05 — 현행 정책에 맞는 처리.** `ChangePgMidContractsService`는 우선 변경 대상 계약의 모든 MID 유형에 대해 동일값을 검사한다. 이후 테넌트 전체 검사에 넣는 후보를 `mids.filter { it.midType.isTerminal() }`로 고른다. `findTerminalMidCollision` 조회도 `pgMid.midType.eq(TERMINAL)` 조건이 있다. 따라서 **단말기→단말기** 같은 값은 테넌트 전체에서 차단하지만, **수기→다른 계약의 단말기 값**은 변경 대상 계약 안에 충돌이 없으면 허용한다. `AddPgMidService.processAddTerminalMid`와 `UpdatePgMidUseCase.validateFinalMid`도 단말기 대상일 때 단말기 MID만 테넌트 전체에서 조회한다. Redmine #4220 요청 본문은 MID·KEY 편집 기능을 요구하며 유형을 가로지르는 전역 유일성을 명시하지 않는다. 기존 QA 기대값을 정정했다.

**ADD-10 — 수기 MID 동시성 결함.** 순차 요청의 계약 내 중복은 ADD-02에서 400으로 막혔다. 동시에 시작한 서로 다른 수기 MID의 변경은 화면과 API 경로에서 200/200이었고 동일 계약에 같은 MID 값 두 건이 저장됐다. 로컬 코드에는 계약 잠금·충돌 조회가 있으나 개발계에 배포된 정확한 백엔드 커밋은 확인하지 못했다. 배포 버전과 잠금·DB 유일성 보장을 확인해야 한다.

### ADD-10 백엔드 후속 검증 — 로컬 수정과 개발계 재QA

- #4220 백엔드 본세션에서 MariaDB 12.2.2 / REPEATABLE-READ의 독립 연결 두 개로 원인을 재현했다. `innodb_snapshot_isolation=OFF`에서 계약 잠금 전에 생성된 과거 스냅샷을 일반 중복 조회가 계속 읽어, 두 요청이 같은 MID 값으로 저장되는 현상을 확인했다.
- 백엔드 `fix/issue-4220`의 로컬 커밋 `8eff1738ea`는 `PgMidQueryRepository.kt`의 `hasContractMidCollision`과 `findTerminalMidCollision`에 `PESSIMISTIC_WRITE` 잠금 조회와 기존 3초 잠금 대기 설정을 적용했다. 수정 조회의 실제 SQL에 `FOR UPDATE`가 있음을 테스트로 확인했다. 계약 선잠금과 최대 1건 중복 조회는 유지했다.
- 본세션 보고: 같은 계약 유지와 계약 이동 경로를 각각 3회씩 수정 전후 검증해 총 12회 경합 실행. 수정 후에는 한 요청만 성공하고 다른 요청은 중복 감지 후 롤백되어 최종 1건, 실패 건의 MID 값·계약이 원본으로 남았다. 신규 MariaDB 테스트 8건의 XML은 `tests=8, failures=0, errors=0, skipped=0`으로 확인했다. 기존 테스트를 포함한 78건 통과는 백엔드 본세션의 검증 결과다.
- `innodb_snapshot_isolation=ON`에서는 잠금 조회가 `errorCode 1020`으로 두 번째 트랜잭션을 롤백해 최종 중복은 생기지 않았다. 이 설정에서 API 오류 응답과 재시도 안내 문구는 미검증이다. 개발계의 실제 DB 버전·격리 수준·옵션값 및 실행 커밋도 미확인이다.
- **2026-10-13 당시 개발계 판정은 ADD-10 FAIL.** 당시 두 브라우저가 200/200으로 완료되어 동일 계약의 수기 MID 두 건이 같은 값으로 저장된 것은 실제 실패다. 다음 절의 배포 후 결과와 구분한다.

### ADD-10 배포 후 재QA — 변경 기준 적용 PASS

- 사용자로부터 배포 완료를 전달받아 TypeScript/Playwright의 독립 Chromium 컨텍스트 두 개로 다시 실행했다. 대상은 모두 **수기(KEYIN)** MID다. 실제 개발계 백엔드 커밋 SHA와 DB 설정은 별도로 확인되지 않았다.
- 같은 웰컴페이먼츠 계약 안의 MID 두 건을 동일 새 값으로 동시 저장한 **3회(KEEP-1~3)**, 루시페이먼츠 계약의 MID 두 건을 같은 웰컴페이먼츠 계약으로 동일 새 값과 함께 동시 이동한 **3회(MOVE-1~3)** 모두 한 요청은 200, 다른 요청은 **500 `Unknown internal server error`**였다. 최종값은 각 회차 1건만 존재했고 실패 건의 MID 값·계약·연동정보·연결·정산 상세는 원본 그대로였다. 테스트 후 모두 복원했다.
- 순차 중복 거절 400, 다건 요청 중 한 건 실패 시 전체 취소 400, 선택 MID 두 건의 값 교환 200, 이동·기존 계약 혼합 선택 200을 추가 재확인했다. 최종 5건의 값·계약·유형·연동정보 설정값·연결·정산 상세를 사전값과 대조해 일치했다.
- 실패 요청의 화면에는 `Unknown internal server error`가 표시됐다. 백엔드 본세션은 개발계 Grafana Loki에서 `pg_mid`의 SQL Error 1020 (`Record has changed since last read`) → `JpaSystemException` → 전역 예외 처리의 500 응답을 확인했다고 보고했다. 실제 실행 Git SHA와 DB `innodb_snapshot_isolation` 변수값은 확인되지 않았다.
- **사용자 변경 판정 기준(2026-10-13):** 두 동시 요청 중 한 건이 정상 저장되고 다른 한 건이 에러로 실패 처리되며, 최종 동일 MID가 1건이고 실패 건의 원래 MID·계약·메타데이터·연결 상태가 유지되면 ADD-10은 PASS다. 500 `Unknown internal server error` 자체는 이 항목의 실패 조건에서 제외한다. 위 6회 실측 결과가 모두 이 기준을 만족해 **ADD-10을 PASS로 정정**했다. 실패 요청을 저장 성공으로 간주한 것은 아니다. 최초 200/200·중복 2건의 실패 이력은 보존한다.
- 저장 직후 TID/MID 관리 재검색에서는 목록 로딩이 20초 넘게 끝나지 않아 이번 회차의 양쪽 화면 재조회는 판정하지 않았다. MID 상세 API 재조회 및 최종 복원은 완료했다. 기존 ADD-11의 통과 근거는 별도 실행 결과다.
- 비공개 원시 결과: `qa-results/issues/4220/dev/postfix-reqa/results.json`, 브라우저 캡처 `KEEP-*.png`, `MOVE-*.png`. 화면 오류 문구 확인은 `postfix-reqa/message-check.json`과 캡처에 남겼다. 재검색 시도 자료는 `postfix-reqa-screencheck-KEEP-2/`와 `postfix-reqa-screencheck-MOVE-1/`에 남겼다. 이 파일에는 내부 식별자와 응답 정보가 있어 외부 게시 대상이 아니다.

### ADD-10 백엔드 409 응답 수정 이력 — 철회

- 백엔드 본세션의 2026-10-13 인계에 따르면, 개발계 Grafana에서 MariaDB SQL errorCode `1020` / SQLState `HY000` (`Record has changed since last read in table pg_mid`)이 Hibernate `GenericJDBCException`, Spring `JpaSystemException`을 거쳐 전역 예외 처리의 일반 500으로 반환되는 경로를 확인했다.
- 본세션은 일괄 계약 변경 서비스에서 **해당 DB 충돌 원인만** `PgMidChangeConflictException`으로 바꿔 HTTP 409와 `다른 요청으로 MID 정보가 변경되었습니다. 다시 조회한 후 저장해주세요.`를 반환하도록 로컬 수정했다고 보고했다. 일반 중복 검증은 400, 다른 DB 오류는 500을 유지한다. 전역 예외 처리와 프론트는 수정하지 않았다.
- 본세션의 로컬 검증 보고: 사용자 지정 `:modules:app:excel:compileKotlin` 성공, 관련 테스트 **92건 실패 0·스킵 0**(서비스 44, Controller 9, 요청 검증 13, 대상 계약 1, 알림 12, H2 저장소 5, 실제 MariaDB 8). Controller 검증에는 실제 MockMvc와 전역 예외 처리의 409/500/400 응답이 포함됐다. MariaDB 12.2.2 REPEATABLE-READ에서 계약 유지·이동 경합, 원본 보존, `innodb_snapshot_isolation=ON`의 실제 1020과 변환을 확인했다고 한다. 이는 **로컬 결과**이며 개발계 재QA 증거가 아니다.
- 위 409 수정은 한때 로컬 커밋 `e04167afcf`까지 작성됐으나, 사용자가 과하다고 판단해 **undo를 요청**했다. 백엔드 본세션은 작업 트리를 중복 방지 잠금 조회 커밋 `8eff1738ea`로 되돌렸다고 보고했다. **409 처리는 배포하지 않으며 ADD-10 PASS의 근거도 아니다.** PASS는 수정 배포 후 기존 6회 개발계 실측에서 중복이 막히고 실패 건의 원본이 유지된 사실 및 변경된 판정 기준에 따른다.

## 복원과 판정 경계

- 각 성공·실패 재현 뒤 변경한 MID의 **값, 계약, 동적 연동정보의 설정 ID와 값, 연결 사용자/가맹점, 상세 정산 정보**를 확인했다. 종료 시 보존한 9건 모두 기준값과 일치했다.
- 연동정보를 다시 저장하면서 내부 행 ID는 새로 발급됐다. 공개 기능상 설정·값은 복원됐지만 내부 행 ID까지 원래 값으로 되돌린 것은 아니다. 변경 이력·알림 발행 이력도 테스트 전 상태로 지우지 않았다.
- ADD-09의 정산 이관 비활성 분기는 담당자 개인확인 대상으로 남겼다. 선택 입력 항목 삭제와 실제 결제·PG 승인은 합의한 제외 범위다.
