[Feat] 매물 수정·삭제·상태 전이 API - #19
Open
RootToApex wants to merge 8 commits into
Open
Conversation
테스트 클래스마다 MySQL·Redis 컨테이너 선언과 DynamicPropertySource 블록이 그대로 복사돼 있어, 도메인이 늘어나는 만큼 같은 블록도 늘어난다. IntegrationTestSupport로 옮기고 하위 테스트가 상속받게 한다. 컨테이너는 static 초기화 블록에서 직접 띄우고 내리지 않는다. @testcontainers는 컨테이너 수명을 테스트 클래스 단위로 관리해 클래스가 끝날 때마다 내렸다 다시 띄우는데, 그때 매핑 포트가 새로 잡힌다. 스프링 컨텍스트는 클래스 사이에 캐시되므로 사라진 옛 포트를 붙잡고 연결 거부가 난다. 클래스마다 컨테이너를 따로 두면 드러나지 않다가 공유하는 순간 터진다. ServiceConnection으로 접속 정보를 넘겨 url·계정·포트 수동 주입을 없앤다. 해당 애노테이션이 spring-boot-testcontainers 모듈 소속이라 의존성을 추가한다.
매물 분류를 enum이 아니라 테이블로 둔다. 카테고리는 매물 필터·가격통계·챗봇이 공유하는 축이라 값이 늘거나 표시명이 바뀔 때 배포 없이 반영돼야 한다. 대신 모든 참조는 숫자 id가 아니라 code로 한다 - 시드 id는 환경마다 달라서 id로 참조하면 로컬에서 되던 것이 운영에서 깨진다. 시드는 기동 시 code 기준 upsert로 반영한다. 로컬은 create-drop이라 재기동마다 테이블이 비고, 카테고리가 비면 매물 등록이 전부 실패한다. data.sql은 로컬에서만 돌고 운영(validate + Flyway)에서는 돌지 않아 두 환경이 갈라진다. 정의에서 빠진 분류는 삭제하지 않고 비활성으로 내린다 - 매물·통계가 참조 중이라 지우면 고아가 된다. 응답은 평면 리스트이되 대분류별로 묶어 내린다. depth와 sortOrder만으로 정렬하면 각 대분류의 첫 자식들이 한 덩어리가 되어 형제가 흩어진다. 가격통계의 categorySnapshot이 categories 부재로 항상 null이던 제약이 이 테이블로 풀린다.
DIGITAL_MOBILE을 쓰고 있었으나 카테고리 마스터의 스마트폰 code는 DIGITAL_PHONE이다. code는 매물·통계가 분류를 참조하는 불변 식별자라 정의와 어긋나면 존재하지 않는 분류를 가리키게 된다. categorySnapshot 예시의 부모도 실제 대분류인 DIGITAL로 맞춘다.
public_id에 ULID를 쓴다. 이 값은 UNIQUE 세컨더리 인덱스인데 난수를 넣으면 INSERT마다 인덱스 곳곳에 흩어져 꽂혀 페이지 분할이 잦아진다. ULID는 앞 48비트가 시각이라 사전순이 곧 시간순이고 새 값이 인덱스 끝에 붙는다. 대가로 생성 시각이 값에 드러나므로, 시각이 어차피 공개되는 대상에만 쓴다. 직접 구현하지 않고 라이브러리를 쓴다 - 비트 배치와 Crockford Base32, 같은 밀리초 내 단조 증가 보장을 손으로 짜면 틀리기 쉽다. CursorCodec은 인터페이스만 있고 구현체가 없었다. 주석이 남긴 세 후보(암호화 토큰 / Redis 발급 이력 / 값 인코딩) 중 마지막을 택한다. API 명세서의 응답 예시가 Base64로 감싼 JSON이라 그 계약을 따른다. 커서는 비밀이 아니지만 신뢰하지도 않는다 - 조회는 언제나 공개 조건을 다시 걸고 커서는 시작 위치만 정한다.
상태 머신은 EnumMap 전이표로 규칙을 한 곳에 모은다. 다만 이건 동시성 방어가 아니다 - 서버 두 대가 동시에 다른 전이를 시도하면 각자 자기 메모리에서 검증해 둘 다 통과한다. 최종 심판은 조건부 UPDATE이고, 그건 상태 전이 API에서 붙인다. 검증 구간은 feature flag로 둔다. 검증 도메인이 나중에 붙기 때문에 초기 상태를 하드코딩하면 그때 등록 코드를 다시 써야 한다. 기본은 꺼둔다 - 켜면 등록이 검증 대기에서 멈춰 아무도 매물을 공개할 수 없다. 목록은 커서 페이지네이션이다. OFFSET은 앞의 n건을 세고 버려 뒤로 갈수록 느려지고, 조회 중 새 매물이 들어오면 항목이 밀려 중복·누락이 생긴다. 노출 조건은 허용 목록으로 짠다 - 거부 목록이면 나중에 상태가 추가될 때 검토 없이 자동 공개된다. 가격은 DTO와 엔티티 양쪽에서 막는다. DTO만 믿으면 이벤트 수신·배치처럼 컨트롤러를 안 거치는 경로로 잘못된 값이 들어오고, 그 한 건이 가격 통계를 오염시킨다. seller_id에 FK를 걸지 않는다 - users는 인증 도메인 소유인데 아직 엔티티가 없다. 여기서 먼저 정의하면 남의 도메인 스키마를 선점하게 된다. V1 마이그레이션에서 건다. 보안 화이트리스트의 매물 경로를 /api/v1/listings에서 /api/listings로 맞춘다. API 명세서와 이미 머지된 시세 조회가 v1 없는 경로를 쓰고 있어 접두어가 갈려 있었다.
전이표를 정책이 명시한 것만 남긴다. 차단된 매물을 삭제로 내릴 수 있게 열어뒀는데, 제재 건을 지우면 거래 스냅샷·주문·신고 이력이 고아가 되고 이의제기 시 근거가 사라진다. 초안과 검증 대기에서 바로 삭제하는 전이도 정책에 없어 닫는다 - "그 외 전이 금지"가 원칙이라 있으면 편할 것 같은 경로를 미리 열어두지 않는다. 검증 대기에서 차단으로 가는 전이를 추가한다. 판정 점수가 차단 구간이면 공개되지 못하고 차단돼야 하는데 그 경로가 없었다. 목록과 상세의 노출 기준을 나눈다. 하나로 묶어 ACTIVE만 열었더니 팔린 매물의 상세가 404가 됐다. 거래 당사자가 나중에 무엇을 샀는지 확인하고 채팅·신고에서 넘어온 링크가 죽지 않아야 하므로, 상세는 SOLD까지 열고 차단·삭제만 없는 것으로 취급한다. 가격 제약을 DB에도 건다. 정책이 DTO 검증과 DB CHECK의 병행을 요구한다 - 컨트롤러를 안 거치는 경로로 들어온 값 하나가 가격 통계 전체를 오염시키기 때문이다. Hibernate의 @check는 7에서 deprecated라 Jakarta Persistence 3.2 표준을 쓴다. 애노테이션이 실제 DDL에 반영되는지 네이티브 UPDATE로 확인하는 테스트를 함께 둔다. 상세 응답의 카테고리·지역을 명세서대로 중첩 객체로 바꾼다.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 개요
매물 P0 나머지 — 수정 / 삭제 / 상태 전이(판매자 수동) / 상세 조회수.
#16(매물 등록·목록·상세) 위에 쌓여 있습니다. #16이 머지되면 이 PR의 diff는 이번 변경만 남습니다.
이미지 변경 시 재검증 회귀, 찜 유저 알림 이벤트, 홀드(결제 중) 차단은 각각 이미지·알림·결제
도메인이 필요해 이번 범위에서 뺐습니다(아래 참조).
🛠️ 작업 내용
상태 전이 — 전이표 + 조건부 UPDATE
MARK_SOLD/RESTORE_MANUAL_SOLD다른 전이를 시도하면 둘 다 통과하고 마지막에 쓴 쪽이 이깁니다.
WHERE에 기대 상태를 실어DB가 한 쪽만 성공시키게 했습니다 — 8스레드 동시 호출에서 정확히 1건만 성공하는 테스트가 있습니다
sold_source=MANUAL로 남깁니다. 결제 건과 되돌리기 규칙이 다르고, 가격 통계는 결제 건만원료로 써야 가짜 거래로 시세를 조작할 수 없습니다
수정
ACTIVE/PENDING_VERIFICATION에서만version필수 → 불일치 시 409. 응답은 flush 후 값이라 그대로 다음 수정에 쓸 수 있습니다행위"라서 인상·동일가는 세지 않습니다. 하루 경계는 KST입니다(저장은 정책대로 UTC,
판정 기준은 사용자가 체감하는 날짜)
삭제
BLOCKED는 삭제 불가(403) — 제재 건을 지우면 거래 스냅샷·주문·신고 이력이고아가 되고 이의제기 근거가 사라집니다
조회수
@Version이 함께 증가해, 남이 상세를 열어본것만으로 판매자의 수정이 낙관적 락 충돌로 실패합니다
에러 코드
LST001가격 인하 한도 초과 — 매물 도메인 첫 코드입니다.AGENTS.md가 접두어를"도메인 착수 시 팀 합의"로 정해뒀으니 확인 부탁드립니다(단일 문자 금지 규칙은 지켰습니다)
검증
clean build114건 통과 (JDK 21)400 LST001/ 낡은 version →409 C009/ version 0→1→2409 C004/ 남의 매물 →403 C006soldSourcenull 초기화📌 다른 도메인이 없어 못 한 것
image_version+1·PENDING 회귀