Repository navigation
Conversation
- WishRepository.countByIdsAndUserId 와 구현·JPA 메서드 삭제 - 원인: 호출처 0건
- TournamentRepository.findBySourceTournamentId 와 구현·JPA 메서드 삭제 - 원인: 호출처 0건 - TournamentException.playLinkAlreadyCreated 삭제 - 원인: #980 에서 이 사유의 발생이 사라져 호출처 0건 - 에러 코드 TOURNAMENT-025 는 결번으로 유지
- item_parse_outbox 가 버전 대신 요청자와 입력을 가리킴
- 이전: item_snapshot_id 유니크로 대기 버전 하나를 가리킴
- 원인: 버전을 파싱 성공 때만 만들어 파싱 전에는 가리킬 버전이 없음
- requester_type · requester_id 로 위시 또는 토너먼트 아이템을 가리킴
- source_url 은 사용자가 넣은 원본
- source_url_without_tracking 은 같은 URL 재요청 판정에 씀
- utf8mb4_bin 으로 대소문자를 구분함
- 원인: 단축 코드는 대소문자를 가림
- source_image_key 유니크로 같은 이미지의 중복 확정을 막음
- wishes · tournament_items 의 item_id 를 NULL 허용으로 바꿈
- 원인: 아이템을 파싱 성공 때 확정함
- 기존 tournament_items 의 item_id 를 가리키던 버전의 아이템으로 채움
- 원인: 기존 행은 snapshot_id 로만 상품을 가리켜 새 칸이 비면 아이템도 요청도 없는 카드가 됨
- 결과: 로컬 MySQL 100만 행에서 14초에서 50초. 도는 동안 이미 채운 행의 UPDATE 는 대기함
- 조건: 전환 배포는 계획 중단 한 번이라 단일 UPDATE 로 둠
- tournaments.started_at 추가
- 원인: 출전 값을 따로 고정하지 않고 시작 시각 기준으로 계산함
- item_snapshots.status 와 두 테이블의 snapshot_id 를 NULL 허용으로 바꿈
- 원인: 더 쓰지 않는 칸이고 삭제는 기존 행 백필(10단계) 뒤로 미룸
- items.canonical_hash 를 DB 생성 컬럼으로 바꿈
- 원인: 원문에서 DB 가 만들어 원문과 해시가 어긋날 수 없음
- 범위: 이 커밋만으로는 테스트가 통과하지 않음. 엔티티와 조회는 뒤 커밋에서 맞춤
- 몰별 쿼리 전체 제거를 없애고 이름이 확인된 파라미터만 지움
- 이전: 무신사 등 6개 도메인은 쿼리를 통째로 지움
- 원인: 상품을 가르는 파라미터까지 지우면 다른 상품이 한 아이템으로 합쳐짐
- 결과: 실제 URL 의 변형 170건을 요청해 보니 추적 파라미터는 어떤 조합으로 지워도 같은 상품이 열림
- 목록은 몰을 가리지 않는 전역 하나로 둠
- 원인: 같은 이름은 어느 몰에서든 뜻이 같음
- 대안: 몰별 목록
- 기각: 몰이 늘 때마다 손으로 옮겨 적게 됨
- pid 는 af_ 파라미터가 같이 있을 때만 지움
- 원인: AppsFlyer 에서는 유입 경로이지만 상품 번호를 pid 로 주는 몰이 있음
- 해시 계산 제거
- 원인: items.canonical_hash 를 DB 가 만듦
- value class 로 바꿈
- 원인: 필드가 url 하나만 남아 손으로 쓴 equals · hashCode 가 필요 없어짐
- 대안: data class
- 기각: copy() 가 private 생성자를 우회해 정규화를 거치지 않은 값을 만들 수 있음
- 저장 상한 2048 을 ProductLink.MAX_LENGTH 하나로 합침
- 이전: CanonicalLink 가 같은 숫자를 따로 가짐
- product.domain 에서 item.domain 으로 옮김
- 원인: 아이템의 정체성을 정하는 클래스이고 쓰는 곳도 item 뿐임
- 후속: 목록의 런타임 조정은 #1183
- 범위: 이 커밋만으로는 빌드되지 않음. 쓰는 쪽은 뒤 커밋에서 맞춤
- ProductLink.parse 가 host 없는 주소를 형식 오류(LINK-001)로 거부함 - 이전: 통과시키고 쓰는 쪽이 각자 null 을 처리함 - 원인: "https:foo" 나 호스트에 밑줄이 든 주소는 URI 로는 읽히지만 host 가 없음 - normalizedHost 를 non-null 로 바꿈 - 결과: 도메인 매칭 · 출처 몰 표시 · 정체 URL 조립에서 null 분기가 사라짐
- ItemParseOutbox 가 요청자와 입력을 듦 - 이전: itemSnapshotId 로 대기 버전 하나를 가리킴 - 원인: 버전을 파싱 성공 때만 만들어 요청 시점에는 가리킬 버전이 없음 - ParseRequester 는 타입과 id 로 위시 또는 토너먼트 아이템을 가리킴 - 원인: item 도메인이 위시와 토너먼트를 모르는 채로 요청자를 식별함 - ParseInput 은 링크 또는 이미지 키 - 링크 입력은 원본과 추적 파라미터를 뺀 URL 을 함께 저장함 - 원인: 같은 URL 을 다시 요청했는지 판정하는 데 씀 - 범위: 이 커밋만으로는 빌드되지 않음. 저장소와 서비스는 뒤 커밋에서 맞춤
- ParseInput 을 타입(LINK · IMAGE)과 값 한 쌍으로 바꿈 - 이전: Link 와 Image 두 클래스를 sealed interface 로 묶음 - 원인: 요청자(타입과 id)와 같은 모양이라 감싸는 클래스가 필요 없음 - item_parse_outbox 의 입력 칸을 input_type · input_value 로 합침 - 이전: source_url · source_image_key 중 하나만 채우고 null 로 종류를 구분함 - 결과: 엔티티의 "둘 중 하나" 검사가 사라짐 - 이미지 중복 확정은 생성 컬럼 image_key 의 유니크로 막음 - 원인: 링크는 여러 번 요청할 수 있어 input_value 에 유니크를 걸 수 없음 - 결과: 로컬 MySQL 에서 같은 이미지 키 재삽입은 1062 로 거부되고 같은 링크 재삽입은 통과함 - 결과: 등록된 키 조회가 이 유니크 인덱스를 range 로 탐 - source_url_without_tracking 을 url_without_tracking 으로 바꿈 - ProductLinkConverter 삭제 - 원인: ProductLink 를 칸에 직접 담는 곳이 없어짐 - 범위: 이 커밋만으로는 빌드되지 않음. 쓰는 쪽은 뒤 커밋에서 맞춤
- item_parse_outbox 에 purpose(REGISTER · REFRESH) 칸 추가 - 이전: 결과가 올 때 요청자에게 아이템이 있으면 새로고침으로 봄 - 원인: 첫 파싱 중에 수동 보정으로 아이템이 먼저 생기면 뒤늦은 등록 성공이 새로고침 완료로 알림이 감 - 알림 종류와 새로고침 실패 로그를 purpose 로 가름 - 기존 아이템을 쓸지 찾거나 만들지는 계속 요청자의 아이템 유무로 봄 - 원인: 등록 요청이라도 그 사이 아이템이 생겼으면 그 아이템에 버전을 붙여야 함 - 대안: 요청자 타입에 목적을 섞음(WISH_REFRESH) - 기각: 같은 위시가 두 요청자가 되어 최근 요청 조회가 두 타입을 뒤져야 함 - 범위: 이 커밋만으로는 빌드되지 않음. 쓰는 쪽은 뒤 커밋에서 맞춤
- Item 은 정체 URL 하나만 가짐 - 이전: 등록 URL · 이미지 키를 들고 정체 URL 을 나중에 채움 - 원인: 아이템을 파싱 성공 때 만들어 그때 이미 정체 URL 을 앎 - 결과: canonicalUrl 이 생성자 val 이 되고 재확정 검사가 사라짐 - ItemSnapshot 에서 status 를 빼고 필드를 불변으로 바꿈 - 이전: PENDING 행을 먼저 만들고 결과로 채움 - 원인: 버전을 성공 · 수동 보정 때만 만듦 - ItemStatus 를 저장하지 않고 계산하는 값으로 바꿈 - PARSING · FAILED · INCOMPLETE · READY - 버전이 될 수 없는 추출 결과 판정을 ItemSnapshot.accepts 로 모음 - 이전: 칸 길이 검사를 ProductSnapshot 이 숫자를 베껴 가지고 함 - ProductSnapshot.link 제거 - 원인: 읽는 곳 0건 - 수기 보정 값의 병합과 필수값 검사를 ManualValues 로 분리 - 원인: 아이템을 확정하기 전에 검증해야 파싱 중 수동 보정이 500 이 되지 않음 - ItemVersions 에 시각 기준 조회(asOf) 추가 - 원인: 시작한 토너먼트의 값을 고정 버전 없이 시작 시각으로 계산함 - 별칭(ItemLink) 삭제 - 원인: 등록을 항상 파싱하게 되어 정체 URL 만으로 아이템을 찾음 - 범위: 이 커밋만으로는 빌드되지 않음. 저장소와 서비스는 뒤 커밋에서 맞춤
- items.source_type 추가: LINK · IMAGE
- 이전: canonical_url 이 비었는지로 링크와 이미지를 가름
- 이미지 아이템의 canonical_url 에 이미지 키 저장
- 원인: 크롭 제거 뒤에도 이미지 키가 아이템의 정체 값으로 남음
- 옛 행 백필
- source_image_key 가 있는 행의 종류는 IMAGE
- source_image_key 가 있는 행의 canonical_url 에 그 키 복사
- 나머지 행의 종류는 LINK
- 범위: canonical_url 이 빈 옛 링크 행은 바꾸지 않음
- 후속: 컷오버 백필
- 생성 경로를 Item.ofLink · Item.ofImage 둘로 좁힘
- 결과: 정체 값 없는 아이템을 새로 만들 수 없음
- 출처 링크와 새로고침 입력을 Item.sourceLink · Item.refreshInput 으로 나눔
- 원인: 새로고침 가능 여부는 정체 값 유무가 아니라 링크로 등록했는지로 정함
- 결과: 이미지 아이템 새로고침 거부 코드를 WISH-007 에서 ITEM-007 로 이관
- 파싱 뒤 raw 원본(items/raw/) 삭제 제거
- 원인: 이미지 키가 아이템의 정체 값이라 원본이 아이템과 같이 남아야 함
- items/raw/ 1일 만료 lifecycle 제거 (prod·dev 버킷, apply 완료)
- 위험: 등록에 매이지 못한 raw 가 쌓임
- 후속: 등록 여부로 가르는 정리 수단
- ImageStorage.delete 는 프로필 raw 회수에만 남음
…ccess-version # Conflicts: # src/main/kotlin/com/depromeet/piki/image/service/ImagePresignService.kt # src/main/kotlin/com/depromeet/piki/item/service/AsyncImageParsingWorker.kt # src/main/kotlin/com/depromeet/piki/item/service/ItemParsingMetrics.kt # src/main/kotlin/com/depromeet/piki/product/service/remote/HttpProductLinkExtractor.kt # src/main/kotlin/com/depromeet/piki/product/service/remote/RemoteExtractionProperties.kt # src/test/kotlin/com/depromeet/piki/item/service/ParsingHeartbeatIntegrationTest.kt # src/test/kotlin/com/depromeet/piki/product/service/remote/ExtractionErrorCatalogTest.kt # src/test/kotlin/com/depromeet/piki/product/service/remote/HttpLinkSnapshotExtractorTest.kt # src/test/kotlin/com/depromeet/piki/wishlist/controller/WishlistRegisterAsyncIntegrationTest.kt
- 위시·토너먼트 아이템의 itemId 를 nullable 로 바꿈
- 이전: 등록 즉시 아이템과 버전을 만들어 둘 다 non-null
- resolveItem 으로 null 에서 값으로 한 번만 바뀜
- 기다리는 버전 칸(waitingSnapshotId)과 출전 pin(snapshotId) 제거
- 원인: 무엇을 기다리는지는 요청자를 가리키는 아웃박스가 말함
- 원인: 출전 값은 pin 대신 토너먼트 시작 시각까지의 버전으로 계산
- tournaments.started_at 추가. start() 가 기록
- 각 엔티티가 자기 requester 와 cardQuery 를 만듦
- CardQuery 는 item.domain 으로 이동
- 원인: 도메인이 service 패키지를 참조하지 않게 함
- ownerTournamentUserId · inviteExpiresAt 을 private var _x + val x 에서 var x protected set 으로 바꿈 - 원인: open 프로퍼티의 private set 금지를 돌아가려 쓴 모양. protected set 이 같은 효과를 한 선언으로 냄 - 결과: 컬럼 이름을 암시 변환에 맡겨 @column(name=) 삭제 - 목록 JPQL 의 t._ownerTournamentUserId 를 t.ownerTournamentUserId 로 교체
- 아이템 조회는 정체 값 하나(findByCanonicalUrl)
- 생성 컬럼 canonical_hash 를 읽기 전용으로 매핑하고 앱이 같은 SHA-256 바이트로 찾음
- 대안: native 로 UNHEX(SHA2(:url, 256)) 비교
- 기각: 엔티티에 없는 칸을 쿼리가 알아야 읽힘
- 조건부 claim · 이미지 키 조회 · 소프트 삭제 · reparent 삭제
- 원인: 아이템은 파싱 성공 때 찾거나 만들 뿐 나중에 바뀌지 않음
- 아웃박스 image_key 생성 컬럼도 읽기 전용 매핑. 등록된 키 조회를 JPQL 로 교체
- 별칭 레포지토리(ItemLink*) 삭제
- 아웃박스에 요청자별 최신 행 조회 추가
- 10만 행 EXPLAIN: 요청자 인덱스 range + 종속 서브쿼리 ref
- 같은 URL 요청 여부는 요청자(위시·출전)로 먼저 좁힌 뒤 url_without_tracking 비교
- 10만 행 EXPLAIN: 사용자 인덱스 50행 x 요청자 인덱스 1행. URL 인덱스 없음
- 위시·출전의 기다리는 버전 조회와 출전 pin 조회 삭제
- 결과: WishOwnerView · TournamentItemRoutingView · TournamentItemUserRoutingView 소멸
- 아이템 여러 건 행 락은 id 순서로 잠금
- 원인: 토너먼트 시작과 버전 생성이 같은 아이템들을 잡을 때 교착 방지
- 지워진 위시·출전에도 파싱 결과를 기록하는 행 락 조회 추가
- 대시보드 집계는 아웃박스의 요청 입력 기준으로 출처를 가름
- 범위: 재시도 집계는 attempt_count · status 를 안 써 새 행이 잡히지 않음. 3단계에서 재연결
- ProductLink.MAX_LENGTH 2048 에서 768 로 변경 - 원인: utf8mb4 유니크 인덱스 키 상한 3072B = 768자 - 결과: 요청 검증·아웃박스 입력·정규화 URL 칸이 같은 값 - prod 실측(2026-10-08): 정규화 URL 최대 603자, 등록 원문 994건 중 768 초과 1건 - items.canonical_hash 생성 컬럼과 해시 조회 삭제 - 이전: 앱이 DB 식과 같은 SHA-256 을 계산해 조회 - canonical_url 에 유니크 직접 적용 - 대소문자만 다른 경로가 같은 상품으로 묶이지 않게 utf8mb4_bin - 768 초과 등록은 요청 검증에서 400 - 정규화 URL 초과는 서버가 다루지 않기로 한 길이라 error 로그 뒤 FAILED
- image_key 생성 컬럼과 uk_item_parse_outbox_image_key 삭제 - 이전: 같은 업로드의 중복 확정을 DB 유니크로 거부. 링크는 반복 요청이라 input_value 유니크 불가 - 이미지도 링크처럼 요청자 기준 조회로 중복 판정 - 비용: 위시 경로의 동시 확정 2건이 같은 아이템 위시 2개가 될 수 있음. 링크의 현재 동작과 동일 - 아이템 자체는 uq_items_canonical_url 이 하나로 유지 - url_without_tracking 을 canonical_input 으로 교체 - 링크는 추적 파라미터를 뗀 URL, 이미지는 키 그대로 - ItemParseOutbox.canonicalInputOf(ParseInput) 이 두 종류를 한 곳에서 계산 - 토너먼트 출전의 요청 입력 일괄 조회 추가 - 토너먼트 락 아래 사전 필터가 유니크 없이도 직렬화를 맡음 - findRegisteredImageKeys · skippingDuplicateKey · DuplicateKey.kt 삭제 - 원인: 호출처 없음 - 위시·토너먼트 서비스의 호출부 변경은 서비스 층 커밋에 포함
- CanonicalLink 를 NormalizedInput 으로 교체 - 원인: canonical 은 귀결점을 정규화한 아이템의 정체 값인데 아웃박스의 귀결 전 값에도 같은 이름이 붙어 두 뜻이 섞임 - 정규화는 과정, canonical 은 아이템 칸에만 - NormalizedInput.of(ParseInput) 으로 링크·이미지를 한 곳에서 계산 - 이미지는 키 그대로 - ParseInput.normalizedOrNull() 이 저장 상한 초과를 거름 - Item.ofLink · ofImage 를 Item.of(sourceType, normalized) 로 통합 - 아웃박스 칸 canonical_input 을 normalized_input 으로 교체 - 서비스 호출부 변경은 서비스 층 커밋에 포함
- CanonicalInput 추가. 아이템의 정체 값 - NormalizedInput.asCanonical() 로만 만듦. 귀결점의 정규화 값을 정체로 삼는 자리는 리졸버 하나 - toLink() · toParseInput() 으로 상품 주소·새로고침 입력을 스스로 만듦 - NormalizedInput.of(ParseInput) 이 저장 상한 판정까지 맡음 - 상한을 넘으면 null. 호출부는 길이를 보지 않음 - ParseInput.normalizedOrNull() · exceedsStorageLimit 삭제 - 엔티티 칸을 String 대신 value class 로 매핑 - Item.canonicalUrl 은 CanonicalInput, ItemParseOutbox.normalizedInput 은 NormalizedInput - Hibernate 가 value class 칸과 Spring Data 파라미터를 받는 것을 통합 테스트로 확인 - 레포지토리 파라미터도 같은 타입 - 이름이 canonical 인 자리는 CanonicalInput, normalized 인 자리는 NormalizedInput
- ImageKey 추가. presign 발급 형식 검증을 타입이 맡음 - 이전: ImagePresignService.verifyUploaded 가 regex 로 검사하고 안쪽은 String 으로 흐름 - confirm 요청 DTO 의 toImageKeys() 가 경계에서 parse - 프로필 이미지 확정도 같은 경계 - ParseInput 을 sealed 로 교체. Link(ProductLink) · Image(ImageKey) - type · value 는 저장용 파생값. 아웃박스에서 되살릴 때만 ParseInput.of(type, value) - linkOrNull · toLink 삭제. 분기는 when - ProductSnapshot.finalUrl 을 ProductLink 로 교체 - 추출 경계에서 한 번 파싱. 규칙에 안 맞으면 null 이라 등록 입력이 정체가 됨 - 이전: 리졸버가 String 을 받아 다시 파싱 - 서비스·테스트 호출부 변경은 서비스 층 커밋에 포함
- ItemResolver 추가. 파싱 성공 때 정체 값으로 아이템을 찾거나 만듦 - 귀결점의 정규화 값 우선, 없으면 등록 입력. 둘 다 상한 초과면 error 로 FAILED - 같은 새 값의 동시 확정은 uq_items_canonical_url 위반으로 한쪽이 회수 대상 - ItemParsingService 가 요청자(ParseRequesterPort)를 통해 위시·토너먼트 아이템의 아이템 칸을 확정 - item 도메인은 요청자 도메인을 모름. 각 도메인이 포트를 구현 - 버전은 파싱 성공 때만 생성. 버전이 될 수 없는 결과는 버전 없이 FAILED - ItemCard · ItemCardService 추가. 아이템 확정 전후를 한 카드 모양으로 - 이전: ItemDisplayService 가 버전 status 로 상태를 계산 - 토너먼트 시작 뒤에는 시작 시각까지의 버전으로 고정 - 파싱 이벤트가 요청자·소유자를 실음 - 이전: snapshotId 로 수신자를 역추적 - 삭제: ItemSharingService · ItemIdentityRecorder · ItemIdentityMetrics · ItemMerged · ParsingOutcome - 원인: 아이템이 등록 때 생기지 않으니 병합·공유 판정 없음. ParsingOutcome 은 isApplied 만 읽혀 Boolean 으로 교체 - 상품 URL 을 로그에 그대로 찍음 - 이전: safeLogString 으로 host·path 만. 쿼리에 상품 번호가 있는 몰은 추적 불가 - logfmt 줄에서는 따옴표로 감쌈. CLAUDE.md 로깅 규칙 갱신 - 프로필 이미지가 raw 를 사용자 접두로 옮겨 심는 이유를 주석으로
- ParseRequester 를 ItemSlot 으로 교체. 위시·토너먼트 아이템이 파싱이 끝나면 아이템을 받아 담을 자리 - 원인: requester 는 사용자를 떠올리게 하는데 실제로는 아이템 칸을 가진 행. 사용자는 owner 로 따로 있어 겹침 - ItemSlotType · ItemSlotPort · ItemSlotState · ItemSlots 도 같이 - 아웃박스 칸 slot_type · slot_id, 인덱스 idx_item_parse_outbox_slot - 아이템 확정 전이를 fill 로 교체 - 이전: resolveItem. 빈 슬롯을 채운다는 뜻이 이름에 없음 - 사용자를 뜻하는 requesterId(응답 가공·토너먼트 조회)는 그대로
- ItemSnapshotRepository.findAllByItemIdsAsOf 추가. 시작 시각까지의 버전만 읽음 - 이전: 버전을 전부 읽은 뒤 ItemVersions.asOf 가 메모리에서 거름 - 원인: 고정이 데이터 클래스의 필터가 아니라 조회 경계여야 시작 뒤 버전이 서비스에 들어오지 않음 - ItemCardService 가 asOf 유무로 조회 메서드를 고름. 버전 묶음 키는 (itemId, asOf) - ItemCard.current · fixedAt · ItemVersions.asOf · ItemSnapshot.isCreatedAtOrBefore 삭제 - 원인: 호출처 없음
- TournamentItem.cardQuery(tournament) 로 교체 - 이전: cardQuery(startedAt: LocalDateTime?) 를 호출부 6곳이 손으로 넘김. 시작 전 경로 3곳은 null 리터럴 - 원인: 시작한 토너먼트인데 기준 시각을 빠뜨리는 실수가 컴파일을 통과함 - 다른 토너먼트를 넘기면 require 로 실패 - 호출부 변경(토너먼트 서비스·영속 서비스)은 서비스 층 커밋에 포함
- 값 칸(name·imageUrl·price·currency)을 생성자에 @column 과 함께 선언 - 이전: 생성자 파라미터를 본문에서 다시 선언. 옛 행 때문에 nullable 인 source·createdBy 만 본문에 남김 - extractedAt 삭제 - 원인: 버전이 성공 때만 생기므로 createdAt 과 같은 값. 가격 이력 응답은 createdAt 을 실음 - 칸 extracted_at 은 남김. 옛 행 정리 때 삭제 - 값이 하나도 없는 버전 금지를 init 의 require 로 이동 - 이전: extracted 팩토리가 accepts 를 다시 검사 - 팩토리 파라미터 이름을 createdBy 로 통일
- ProductSnapshot.method 를 proto ExtractionMethod 로 교체 - 이전: 경계에서 enum 의 name 을 String 으로 풀고 ItemSnapshotSource.fromWireMethod 가 문자열을 다시 대조 - ItemSnapshotSource.of(method) 는 exhaustive when. 계약에 경로가 늘면 컴파일이 깨져 결정을 강제 - UNSPECIFIED · UNRECOGNIZED · null 은 출처 없음. 런타임 tolerant reader 는 유지 - ItemSnapshotSource 상수 주석을 description 필드로 교체
- ItemSnapshotSource 삭제. 계약 enum ExtractionMethod 를 그대로 저장 - 원인: 한 칸에 "어떻게 뽑았나" 와 "사람이 넣었나" 를 같이 담아 계약에 없는 MANUAL 이 필요했고 그 때문에 enum 과 번역이 따로 있었음 - method 칸은 STRUCTURED · LLM, 수기와 옛 행은 NULL - userInput 칸은 사람이 넣었는지. 옛 행은 false. manual 은 MySQL 예약어라 피함 - 마이그레이션: user_input 추가, MANUAL 행 이관, SERVER · SERVER_LLM 값을 계약 이름으로, source 를 method 로 개명 - 계약 밖 값(UNSPECIFIED · UNRECOGNIZED)은 추출 경계(ProductSnapshot.fromExtracted)에서 NULL - 클라이언트 응답: 카드·가격 이력의 source 를 method · userInput 으로 교체 - 계약 변경. PR 의 클라이언트 변경 목록에 추가
- ItemSnapshot.isSharedValue 삭제. ItemVersions.displayFor 가 기계 READY 조건을 그대로 씀 - 원인: "공유 값" 이 아이템 병합처럼 읽힘. 묻는 건 사용자 입력이 아닌 완성 버전인지 - Tournament.startedAt 에 @column 명시 - ItemSnapshot.manual 팩토리를 userInput 으로 개명. 칸 이름과 같은 말 - init 과 accepts 가 같은 술어(filledFieldCount · fitsColumns)를 씀. 조건식 복제 소멸
- 원칙: 서비스는 non-null 이거나 던지거나 sealed 로 답함. nullable 은 레포지토리의 find 에만 - ItemParsingService.inputOf 가 행이 없으면 error. PROCESSING 행은 있어야 하는 불변식 - 파서가 예외를 잡아 FAILED 로 닫음. 저장값이 파싱 규칙에 안 맞는 경우도 같은 경로 - ItemParsingService.markExtracted 가 ExtractionSettlement 를 돌려줌 - Applied(status) · Superseded(다른 실행이 끝냄) · Discarded(슬롯 사라짐) - 이전: null 이 "기록되지 않음" 을 뜻했고 왜인지는 주석에만 - ItemSlotState 를 sealed 로. Present(owner, itemId) · Gone(탈퇴로 하드 삭제) - WishPersistenceService.registerImage 가 항상 만듦. 이미 요청한 키는 호출부가 먼저 거름 - 토너먼트와 같은 모양. WishRepository.findRequestedImageKeys 추가 - ItemResolver.findByLink 삭제. 중복 판정은 레포가 위시·출전을 아이템 정체 값으로 바로 찾음 - 이전: 아이템을 먼저 찾고 그 id 로 다시 조회 - ExtractionModelSettings.modelOf 를 modelOverrideOf 로 개명. null 이 "지정 없음" 임을 이름에
- wish_memos 테이블 추가 - 행은 쌓이기만 함 - 현재 메모는 written_at 이 가장 늦은 행 - 지우기는 content 없는 행으로 남김 - 원인: 같은 아이템의 위시를 합칠 때 두 위시의 메모를 쓴 시각 순으로 이어야 함 - 결과: 현재 메모 조회가 (wish_id, written_at) 인덱스 역방향 스캔, filesort 없음 (로컬 EXPLAIN) - written_at 을 created_at 과 따로 둠 - 원인: 합칠 때 복사한 행도 원래 쓴 시각을 지켜야 함 - 원인: created_at 은 감사 리스너가 저장 시각으로 채움 - 현재 메모와 내용이 같으면 쌓지 않음 - 원인: 같은 메모를 다시 보내도 이력에 중복이 남지 않게 함 - 기존 wishes.memo 를 새 테이블로 옮김 - 쓴 시각은 위시의 updated_at - 범위: wishes.memo 칸은 남김 - 후속: 옛 인스턴스가 내려간 뒤 차이를 다시 옮기고 칸 삭제 - 탈퇴 cascade 에서 위시보다 먼저 메모 하드삭제 - 메모 수정의 위시 행 잠금 이유 주석 삭제 - 원인: 메모가 위시 행을 고치지 않아 전 컬럼 UPDATE 되덮기 소멸 - 후속: 합치기 작업에서 잠금 이유를 다시 적음
- 위시는 같은 사용자, 출전은 같은 토너먼트 안에서 살아 있는 슬롯 하나만 한 아이템을 가짐
- wishes·tournament_items 에 deleted_at 으로 계산하는 live 칸과 (범위, item_id, live) 유니크 추가
- 지운 슬롯은 live 가 NULL 이라 유니크 검사에서 빠짐
- 대안: (범위, item_id, deleted_at) 유니크
- 기각: 살아 있는 슬롯 쪽이 NULL 이라 살아 있는 슬롯끼리 검사에서 빠짐
- 대안: 앱이 쓰는 live 칸
- 기각: 위시 delete·출전 softDelete·일괄 삭제 JPQL 이 모두 live 를 맞춰야 함
- 결과: VIRTUAL 칸 추가는 INSTANT, 유니크 추가는 INPLACE·LOCK=NONE (로컬 MySQL 8.4)
- 기존 중복 정리
- 위시는 먼저 담은 쪽만 남기고 나머지 soft delete, 메모는 쓴 시각 그대로 남는 위시로 복사
- 결과: prod 2묶음·dev 4묶음 정리 대상 (아이템 합치기 마이그레이션 반영 뒤 읽기 전용 조회)
- 버전을 고정하던 옛 출전(snapshot_id 있음)은 유니크 검사에서 뺌
- 원인: prod 의 끝난 토너먼트 13곳에 같은 아이템 출전이 대진 기록으로 남음
- 대안: 옛 중복 출전 soft delete
- 기각: 토너먼트 상세가 대진 짝을 못 찾아 500
- 대안: 출전에는 유니크 없음
- 기각: 동시 확정 경합을 못 막음
- 비용: snapshot_id 를 지울 때 live 식 재정의 필요
- 합칠 출전 조회는 첫 행을 고름
- 원인: 옛 중복이 살아 있는 대기 토너먼트 존재
- 슬롯 확정이 Filled 또는 AlreadyHeld(남는 슬롯)를 돌려줌
- 같은 아이템을 가진 살아 있는 슬롯이 있으면 이 슬롯을 soft delete
- 위시는 메모 행을 쓴 시각 그대로 남는 위시로 복사
- 지운 슬롯은 합치지 않고 채움
- 원인: 지운 위시의 메모가 남는 위시에 나타남
- 남는 슬롯 조회는 일반 SELECT
- 대안: 잠금 읽기
- 기각: 남는 위시를 잠근 사용자 입력과 교착 (로컬 재현 ERROR 1213)
- 파서가 유니크 위반 때 새 트랜잭션으로 한 번 다시 기록
- 원인: 아이템 잠금을 기다리기 전에 RR 스냅샷이 고정돼 먼저 커밋된 슬롯이 안 보임
- 결과: 정규화 URL 동시 생성 경합도 회수 대기 없이 끝남
- 예외: 유니크 외 무결성 위반은 다시 기록하지 않음
- 메모 수정이 위시를 잠그는 이유 주석 추가
- 같은 아이템의 위시 둘을 전제로 한 출전 담기 테스트에서 중복 위시 제거
- 원인: 살아 있는 위시 둘이 같은 아이템을 가질 수 없음
- 후속: 파싱 성공의 합침 사실 발행·사용자 입력 경로의 합치기·합침 알림
- 파싱 성공이 슬롯 확정 결과를 받아 발행할 사실을 고름 - 채움: 기존대로 완료 또는 일부 완료 - 남는 슬롯에 합쳐짐: 지워진 슬롯·남는 슬롯·주인을 담은 합침 사실 - 이전: 합쳐져 지워진 위시에도 완료 알림 발행 - 합침 사실은 알림 마커 없음 - 원인: 마커만 있고 핸들러가 없으면 디스패처가 실패함 - 후속: 합침 알림 종류·문구를 정한 뒤 마커와 핸들러 함께 추가 - 결과: 합침 사실을 빼면 새 테스트만 실패 (로컬 확인)
- 토너먼트 동기화 전송이 출전 좌표를 수신자 판정기에서 받음
- 이전: 전송기가 슬롯 타입을 비교하고 출전 행을 직접 조회
- 결과: 출전 좌표 조회가 한 곳
- 동기화 실패 로그의 slot 을 slotType·slotId 로 분리
- 원인: ItemSlot 문자열의 공백이 logfmt 를 깸
- 완료 알림 문구 분기를 라우팅 sealed 타입 전수 when 으로 바꿈
- 이전: routingKind 비교와 else
- 옛 수신자 규칙을 적은 에세이 주석을 why 한 줄로 줄임
- 범위: 해소 알림의 같은 판정 두 번과 폴백은 그대로 둠
- 원인: 수신자 조회와 컨텍스트 조회가 DB 를 따로 읽음
- 결과: 경합 때 폴백에 닿음
- 출전 추가·수정 가능 판정을 Tournament 확장 함수 둘로 모음
- checkItemsAddable: PENDING 다음 클론
- checkItemsModifiable: 클론 다음 PENDING
- 이전: isRoot()·sourceTournamentId?.let 두 모양이 서비스 네 곳에 흩어짐
- 범위: 두 판정의 순서 차이는 그대로 둠
- 원인: 클론이면서 시작한 토너먼트의 응답 code 가 순서에 달림
- 출전 저장 서비스의 참여자 확인을 requireParticipant 하나로 모음
- rejectIfWontFit 을 private 로 바꾸고 @transactional 제거
- 원인: 같은 빈 안의 호출 하나뿐
- 결과: 어노테이션이 동작하지 않음
- 시작 결과 조립을 startResultOf 하나로 모음
- 카드 맵 조립을 cardsOf 하나로 모음
- 썸네일도 같은 경로
- 카드 조회 실패 판정을 cardIn 하나로 모음
- 출전 상세의 내 위시 메모 조회를 wishMemoOf 로 뽑음
- 위시에서 담기 판정 주석의 거르는 위치 정정
- 파싱 중인 위시는 위시 조회에서 빠짐
- 원인: 아이템 없음
- 위시 컨트롤러의 toResponse 별칭 제거
- 산문 주석을 why 한 줄로 줄임
- import 순서 정리
- 토너먼트 상세의 출전 status 를 PARSING·FAILED·INCOMPLETE·READY 로 정정
- 이전: 옛 PENDING·PROCESSING
- 조용한 갱신 status 에 INCOMPLETE 추가
- 파싱 완료 알림 설명에서 옛 공유 모델 문장 제거
- 이전: 한 파싱 결과를 여러 사람이 출처별 문구로 받음
- 위시 새로고침 문서 정정
- READY·INCOMPLETE 둘 다 새로고침 가능
- 등록 파싱 중이면 새 추출 없이 현재 상태 반환
- 시작 전 출전 카드는 새 값을 봄
- 시작한 토너먼트의 출전 값은 시작 시각 기준
- 보정 대신 사용자 입력
- 가격 이력에서 빠지는 버전에 일부만 빈 버전 추가
- FAILED 사용자 입력 example 을 사용자 입력 표시값으로 바꿈
- 이전: 서버 추출 READY 샘플
- 출전 sourceUrl 이 성공 뒤 확정된 상품 페이지 주소임을 명시
- 위시에서 담기 409 의 파싱 중·실패 사유와 example 제거
- 원인: 위시 카드는 아이템이 있음
- 결과: 미완성만 걸림
- extractor 내부 구현(LLM·Gemini Vision) 언급 제거
- 위시 응답의 item 을 상품 스냅샷 대신 상품으로 부름
- method·currency 설명에 PARSING·FAILED 시 null 추가
- 같은 말을 반복하는 문서 상수·DTO 주석 삭제
- 보정·수동·공유·병합 어휘를 사용자 입력·기계 READY·합친으로 바꿈
- 테스트 이름·테스트 주석·ItemSnapshot 주석
- 예외: 토너먼트 참여의 합류·공유 링크는 그대로 둠
- 원인: 다른 뜻
- 삭제된 대상을 가리키던 문구 정정
- 스냅샷 접근 메타 테스트 안내의 ItemDisplayService
- 아이템 결정 테스트의 해시 칸
- 등록 테스트의 별칭 주석 삭제
- 400·409 테스트 셋에 code 단언 추가
- 같은 일을 하는 기계 READY 저장 헬퍼 둘을 하나로 합침
- Triple 반환 헬퍼 둘을 data class 로 바꿈
- 행만 저장하는 위시 헬퍼 이름을 saveReadyWish 로 바꿈
- 이전: 파싱 경로를 타는 다른 클래스의 seedReadyWish 와 같은 이름
- 기계 READY 테스트와 같은 옛 출처 테스트 삭제
- 성공과 거부를 함께 단언하던 카드 테스트 둘을 넷으로 나눔
- 범위: ItemSharingIntegrationTest 클래스 이름은 그대로 둠
- 원인: 슬롯 합치기 작업과 같은 파일
- 토너먼트 후보 추가 알림 제목을 "{이름}님이 토너먼트 후보를 담았어요" 로 바꾸는 마이그레이션 추가
- 이전: prod 는 백오피스에서 바꾼 "후보를 담았어요" (2026-08-18)
- 이전: dev 는 처음 시드 문구 "아이템을 추가했어요"
- 결과: prod·dev·새 DB 가 같은 문구
- 토너먼트 파싱 완료 본문을 "토너먼트 후보가 등록됐어요" 로 바꿈
- 원인: 코드 상수라 백오피스로 못 고침
- 이전: "아이템이 등록됐어요" (prod 3건·2명 수신, 10-08 부터)
- 알림 이력 API 문서의 표와 설명을 같은 말로 정정
- 토너먼트 출전을 가리키는 문구는 토너먼트 후보로 바꿈 - 문장에 토너먼트가 이미 있으면 후보만 씀 - 위시에서 고르는 대상을 가리키는 문구는 상품으로 바꿈 - 위시에서 담기 개수 검증·위시에 없는 상품·존재하지 않는 상품 - 위시 에러 문구는 위시로 바꿈 - 원인: 아이템은 코드의 이름. 사용자에게 닿지 않게 함 - 범위: code 번호·API 문서·테스트 이름·코드 이름은 그대로 둠
- 사용자 입력이 아이템 없는 슬롯을 살릴 때도 슬롯 확정을 거쳐 합쳐짐
- 이전: 위시·출전 엔티티를 직접 채움
- 결과: 같은 상품을 가진 살아 있는 슬롯이 있으면 유니크 위반 500
- 위시 응답은 남은 위시
- 같은 요청의 메모도 남은 위시에 저장
- 조건: 클라이언트가 요청 id 와 응답 id 가 다르면 요청한 카드를 지움
- 출전 응답은 그대로 200·본문 없음
- 사용자 입력의 아이템 확정을 UserInputRecorder 하나로 모음
- 이전: 위시·출전 저장 서비스에 같은 규칙 두 벌 (리뷰 #17)
- 호출부 트랜잭션 필수 (MANDATORY)
- 원인: 호출부가 슬롯을 먼저 잠가야 함
- 범위: 이미지 업로드 전 검증용 자리표시 상수 두 벌은 그대로 둠
- 아이템이 있는 슬롯은 포트가 합치기 판정을 건너뜀
- 원인: 그 슬롯이 자기 자신을 holder 로 보고 지워질 수 있음
- 이전: 파서·사용자 입력 호출부 두 곳이 같은 가드를 둠
- 합침 사실 발행을 ItemSlots 로 옮김
- 원인: 파싱 성공과 사용자 입력 어느 길로 합쳐져도 빠지지 않게 함
- 슬롯 확정 결과가 합쳐진 슬롯의 주인을 함께 돌려줌
- 유니크 거부 뒤 한 번 다시 실행을 UniqueConflictRetry 로 모음
- 파서·위시 사용자 입력·출전 사용자 입력이 함께 씀
- 로그 키를 item.parse.retry 에서 item.confirm.retry 로 바꿈
- 원인: 사용자 입력도 아이템 잠금을 기다리기 전에 스냅샷이 고정돼 먼저 커밋된 슬롯이 안 보임
- 결과: 경합 테스트에서 다시 실행을 빼면 유니크 위반으로 실패 (로컬 확인)
- 경합 테스트는 파싱 성공이 아이템 잠금을 쥔 상태를 SQL 로 재현
- 원인: 테스트 계정에 PROCESS 권한이 없어 잠금 대기 표를 못 읽음
- 같은 계정 연결의 실행 중 쿼리로 대기를 확인
- 스냅샷 원본 소비자 동결 목록에서 위시·출전 저장 서비스 제거
- 원인: 사용자 입력 버전 쓰기가 item 패키지로 옮겨감
- 위험: 다른 사용자의 출전에 합쳐지면 입력한 값이 남은 출전 카드에 안 보임
- 원인: 출전 카드는 그 출전 주인의 맥락 값으로 계산함
- 후속: 막을지 둘지 결정
- 알림 종류 ITEM_PARSING_MERGED 추가
- 받는 사람: 합쳐져 사라진 슬롯의 주인 한 명
- 제목: 상품명 (남은 슬롯 주인 맥락의 표시값)
- 원인: 눌러서 가는 남은 카드와 같은 이름이 보여야 함
- 대안: 사라진 슬롯 주인 맥락
- 기각: 직접 입력이 남이 올린 후보에 합쳐질 때 제목과 카드의 이름이 다름
- 본문: 위시 "기존 위시에 합쳐졌어요" · 토너먼트 "기존 토너먼트 후보에 합쳐졌어요"
- 문구는 코드가 소유
- 원인: 템플릿은 타입당 한 행
- 좌표: 남은 슬롯
- 원인: 사라진 슬롯으로 가면 404
- 푸시 보냄
- 템플릿 시드 마이그레이션·푸시 정책·종류 파생·변수 카탈로그 함께 추가
- 합침 사실 ItemSlotMerged 에 알림 마커·itemId·남은 슬롯 주인 추가
- 파싱 성공과 사용자 입력 두 길 모두 알림이 감
- 토너먼트 참여자 전원에게 조용한 갱신 신호 TOURNAMENT_ITEM_MERGED 추가
- 사라진 후보 id·남은 후보 id 를 실음
- 위시끼리 합쳐지면 보내지 않음
- 원인: 다른 참여자 화면에 사라진 후보가 로딩 중으로 남음
- 기존 파싱 동기화 전송과 같은 전달 경로를 씀
- 결과: 좌표를 사라진 슬롯으로 바꾸면 새 알림 테스트 둘 다 실패 (로컬 확인)
- 결과: 제목 맥락을 사라진 슬롯 주인으로 바꾸면 토너먼트 알림 테스트 실패 (로컬 확인)
- 템플릿 변수 샘플값이 핸들러 문구 상수를 참조
- 원인: 문구가 바뀌면 샘플만 낡음
- 후속: 클라이언트에 새 알림 종류·갱신 신호 전달
- 위시에서 담기가 유니크 거부 뒤 한 번 다시 실행
- 이전: 파싱 중이던 같은 토너먼트 후보가 같은 상품으로 동시에 확정되면 유니크 위반 500
- 원인: 확정은 토너먼트 행을 잠그지 않음
- 원인: 담기의 스냅샷이 그 확정 커밋 전에 고정됨
- 결과: 다시 실행하면 먼저 확정된 후보가 보여 이미 담은 후보 409
- 결과: 경합 테스트에서 다시 실행을 빼면 500 (로컬 확인)
- 컨트롤러가 비트랜잭션 TournamentItemService 를 거쳐 담기를 부름
- 원인: 다시 실행은 매번 새 트랜잭션이어야 함
- 경합 테스트는 기존 위시 담기 동시성 테스트에 둠
- 실제 API 로 토너먼트를 만들고 409·code 를 단언
- 실행 중 쿼리 확인을 support 의 runningQueries 로 올림
- 합치기 경합 테스트도 함께 씀
- 슬롯 확정 인터페이스에 경계 규칙을 둠 - 요청 시점에 같은 상품임을 알면 등록 경로가 409 - 아이템이 확정될 때에야 알면 남은 슬롯에 합침 - 원인: 규칙이 위시 등록·후보 추가·위시에서 담기·파싱 성공·직접 입력에 흩어져 한 곳만 읽어서는 안 보임 - 위시 등록·토너먼트 후보 추가의 409 판정에 그 자리를 가리키는 한 줄 추가
- 시작 판정의 버전 조회를 잠금 읽기(PESSIMISTIC_READ)로 바꿈
- 이전: 일반 SELECT 가 트랜잭션의 첫 일반 SELECT 때 찍힌 스냅샷을 씀
- 결과: 아이템 잠금을 기다리는 동안 커밋된 버전이 판정에서 빠짐
- 결과: 그 버전은 started_at 보다 일러 이후 고정 값에는 들어감
- 결과: 시작 응답의 가격·순서와 고정 값이 어긋남. 대진이 가격 순이라 경기 기록 400 가능
- 결과: 방금 직접 입력으로 채운 후보를 미완으로 보고 시작 거부
- 대안: 시작 트랜잭션을 READ COMMITTED 로 실행
- 기각: 고칠 곳은 조회 하나인데 트랜잭션 전체의 읽기 규칙이 바뀜
- 기각: 다른 트랜잭션 안에서 부르면 Spring 이 격리 수준을 오류 없이 무시
- 대안: 아이템 잠금 전의 조회를 모두 잠금 읽기로 바꿔 첫 일반 SELECT 를 잠금 뒤로 미룸
- 기각: 잠금 앞에 일반 조회 한 줄이 들어오면 소리 없이 다시 깨짐
- 비용: item_snapshots 의 item_id 인덱스에 범위 잠금. 새 상품의 첫 버전 넣기가 시작 커밋까지 기다릴 수 있음 (로컬 실측)
- 결과: 300행부터 6만 행까지 후보 2개·32개 모두 item_id 인덱스 범위만 잠금 (로컬 EXPLAIN·data_locks)
- 예외: 20행 표에서 후보가 상품 거의 전부를 덮으면 PRIMARY 전체 스캔으로 표 전체 잠금
- 카드 조회에 잠금 읽기 길 lockedCardsOf 추가. 시작 판정에서만 씀
- 경합 테스트 추가
- 다른 트랜잭션이 아이템 잠금을 쥔 채 새 버전을 넣은 동안 시작
- 결과: 잠금 읽기를 일반 읽기로 되돌리면 옛 가격 [10000, 20000] 으로 실패 (로컬 확인)
- 토너먼트 슬롯의 알림 좌표에서 위시 기본값 제거. 행이 없으면 오류
- 이전: 후보 행을 못 찾으면 위시 종류·위시 id 없음으로 알림 발송
- 원인: 도달할 수 없는 분기
- 후보 행은 소프트 삭제만 있음. 탈퇴도 위시만 하드 삭제
- 조회가 지운 행까지 봄
- 결과: 도달하면 틀린 알림 대신 오류 로그로 드러남
- SSE 동기화가 위시 슬롯을 건너뛰는 판정을 슬롯 타입으로 분리
- 이전: 위시 슬롯과 후보 행 없음을 같은 null 로 건너뜀
- 해소 알림의 같은 기본값은 유지하고 이유 주석 추가
- 원인: 수신자와 수신자별 좌표를 따로 계산하는 사이 카드가 지워지는 경합은 도달 가능
- 결과: 좌표가 없으면 알림 종류도 위시로 정해져 기본값과 같음
- 알림 이력 API 문서의 실패 알림 두 종 refId 를 itemId 에서 요청한 카드 id 로 고침 - ITEM_PARSING_FAILED: wishId 또는 tournamentItemId - ITEM_REFRESH_FAILED: wishId - 이전: (dev) 두 핸들러 모두 refId 에 itemId - 원인: 상품이 파싱 성공 때 확정되어 실패 이벤트에 itemId 가 없음 - 실패 알림 refId 설명 단락 추가 - 이동은 다른 파싱 알림처럼 wishId 또는 토너먼트 좌표 - 이 계약 이전에 발송된 실패 알림은 itemId - SSE payload 주석의 refId 구체 값을 빼고 API 문서 표를 가리킴 - 원인: 계약 표를 복제한 값이 이번처럼 조용히 낡음 - 후속: 클라이언트가 실패 알림 refId 를 상품 id 로 읽지 않는지 확인
- 표를 NotificationType 15종과 1:1 로 맞춤 - 이전: 제목은 전 10종. 행은 13개. ITEM_PARSING_INCOMPLETE·ITEM_PARSING_RECOVERED 행 없음 - 행 순서를 enum 순서로 맞춤 - RECOVERED 의 kind 는 받는 사람 자신의 카드 출처를 따른다는 설명 추가 - body 설명을 시드 마이그레이션 기준으로 고침 - 이전: 파싱 완료와 공지만 값이 있음 - 기본 문구에서 body 가 빈 타입은 토너먼트 알림 7종과 ITEM_PARSING_FAILED - 문구는 백오피스에서 바뀔 수 있다는 전제 추가 - TOURNAMENT_COMPLETED·TOURNAMENT_RESULT_READY 트리거에서 클론 표현 제거 - 템플릿 변수 카탈로그 주석에서 아래 코드를 복제한 타입 목록·개수 제거 - 원인: 파싱 알림 3종 표기가 이미 낡음 - 같은 사실을 담은 옛 주석 정정 - 토너먼트 완료 핸들러의 클론 표현 - RECOVERED 주석의 실패·미완 표현을 미완으로 고침. 판정은 isIncomplete 만 봄 - ItemDisplayName 을 쓰는 알림 목록
- 시작 API 409 설명과 example 에서 "이미 본인 클론이 있음" 제거 - 원인: 멤버 시작이 클론 생성이 아니라 참여 행 전이로 바뀐 뒤(#1027) 던지는 곳 없음 - 쓰이지 않는 alreadyCloned 팩토리와 TOURNAMENT-029 코드 삭제 - 번호는 결번으로 둠. 037 과 같은 처리
- 위시·토너먼트 서비스에 같은 값으로 두 벌 있던 업로드 전 이미지 자리표시 상수를 UserInput.beforeImageUpload 하나로 모음 - 자리표시 값은 UserInput 안에 private 으로 숨김 - 위시 커맨드의 userInput(imageUrl) 을 private 으로 좁힘 - 원인: 바깥 호출부가 사라짐
- 위시 등록·목록 설명에 같은 상품 위시 합침 결과 추가 - 합쳐진 위시는 사라지고 메모는 남은 위시로 옮겨짐. ITEM_PARSING_MERGED 알림 - 위시 사용자 입력 설명에 응답 wish.id 가 요청과 다를 수 있음 추가 - 위시 409 설명에 같은 링크로 담은 파싱 중·실패 위시도 해당함 추가 - example 이름의 공유 정체성 표현 제거 - 위시 새로고침 400 에 code ITEM-007 표기. 실패한 이미지 항목은 409 - presigned 발급 설명 정정 - 위시는 확정 때 생기고 상품은 파싱 성공이나 사용자 입력으로 정해짐 - 이전: S3 lifecycle 이 지움. 이 브랜치에서 items/raw 만료 규칙 삭제 - 이미지 확정 설명에 남아 있는 카드로 이미 확정한 key 는 건너뜀 추가 (위시·토너먼트) - 위시 응답 설명 정정 - 상품 id 는 파싱 성공이나 사용자 입력으로 정해지기 전 null - sourceUrl·sourcePlatform 은 상품이 정해지면 정규화된 주소로 바뀔 수 있음 - 가격 이력은 이름·가격·이미지가 다 있는 버전만. PARSING·FAILED 는 늘 빈 배열 - 토너먼트 후보 추가 설명 정정 - 이전: 아이템이 PARSING 상태로 생성. 200 응답에 item.status - 후보가 파싱 중 카드로 생기고 상품은 파싱 성공이나 사용자 입력으로 확정 - 합침 결과와 그 뒤 404 추가 - 알림 주석 정정 - silent-sync 사건 목록 서술 제거. 사건 종류는 SilentSyncType 이 정본 - RECOVERED 와 완료 알림이 갈리는 기준을 요청한 카드 대 같은 상품의 다른 카드로 고침 - 새로고침 판정을 파싱 요청 목적(REFRESH)으로 고침 - 범위: 이 브랜치 전부터 낡은 문서(클론 서술·옛 에러 코드·중복 400·예시)는 dev 기준 별도 브랜치에서 처리
- notification-sse-spec.md
- silent-sync 에 TOURNAMENT_ITEM_MERGED 행 추가. PARSED status 에 INCOMPLETE 추가
- PARSED 수신자를 요청한 출전 카드의 토너먼트 참여자로 고침
- 이전: 한 아이템이 여러 토너먼트에 있으면 각자 받음
- 분기표에 RECOVERED·MERGED 행 추가. 실패 알림 refId 를 요청한 카드 id 로 분리
- TOURNAMENT_COMPLETED·RESULT_READY 의미에서 클론 표현 제거
- 예시 코드에 RECOVERED·MERGED 알림과 MERGED 신호 분기 추가
- CLAUDE.md·README.md 도메인 용어
- 이전: wish 의 기다리는 행, tournament_item 의 고정 버전, 버전이 상태를 듦
- item 정체성은 링크 정체 URL 또는 이미지 키. 파싱 성공이나 사용자 입력으로 정해짐
- 카드 상태는 버전과 최근 파싱 요청에서 계산. 시작한 토너먼트는 started_at 까지의 버전
- 없어진 이름 교체: AsyncItemParsingWorker·ProductLinkExtractor·toProductSnapshot(link)·ItemSnapshot.pending/manual
- README 의 product·item 이름 규칙이 CLAUDE.md 와 거꾸로 적힌 것 바로잡음
- testing-convention.md stub 예시를 실제 이름(StubLinkSnapshotExtractor)과 필드로 고침
- 원인: 예시대로 쓰면 컴파일 안 됨
- parsing-claim-ownership.md·tournament-match-bracket-spec.md 는 결정 당시 기록이라 본문 유지
- 맨 위에 현 구조와 다르다는 안내 한 줄 추가
- dev 에서 함께 온 문서(직접 수정 없음): terraform-grafana/README.md · terraform/README.md - 실패 code 번역 표에 dev 의 일시 전환(#1241) 반영. 영구 실패는 계약 enum 키 3개 - 결과: 계약 대조 테스트(ExtractionErrorCatalogTest) 실패 해소 - 최종 주소는 http 도 받음(dev 의 parseHop). parseOrNull 과 합쳐 parseHopOrNull 로 둠 - 정체 URL 은 NormalizedInput 이 https 로 다시 조립해 http·https 가 하나로 모임 - 토너먼트 후보 추가·수정 공용 검사가 옛 클론 id 지표(#1191)를 남김. dev 의 rejectCloneId 는 흡수 - 쓰지 않는 예외 팩토리 024·025 는 양쪽 삭제를 모두 받음. 031 코드는 dev 를 따라 삭제 - 위시 우승 칩(#1237)을 WishResult 로 옮김 - 우승 조회는 후보의 itemId 로 바로 거름. 이 브랜치에는 후보의 버전 포인터가 없음 - 정체성 기록기와 그 테스트는 이 브랜치가 지운 채로 둠 - dev 의 새 테스트(parseHop·우승 칩)를 이 브랜치 API 로 고침. 최종 주소 테스트는 http 허용 규칙으로 고침
- 사용자 입력 규칙(ItemCard.userInputValues)이 빈 입력이면 400 ITEM-009 를 던짐
- 이전: 이름·가격·통화·이미지 없이 보내면 지금 보이는 값을 복제한 사용자 입력 버전이 쌓임
- 결과: 가격 이력에 같은 점이 늘고 카드가 사용자 입력 표시로 바뀜
- 공백뿐인 문자열도 없는 값으로 봄
- 원인: 통화만 "" 로 보내면 빈 입력 판정을 지나 복제가 남음(리뷰 지적)
- 위시·토너먼트 후보 직접 입력이 같은 규칙을 써 둘 다 적용
- 위시는 메모만 보낸 PATCH 가 먼저 갈라져 메모만 바뀜
- 대안: 200 으로 아무것도 하지 않음
- 기각: 클라이언트 버그가 조용히 묻힘
- 대안: 위시 요청 DTO 에서만 막기
- 기각: 토너먼트가 그대로 남음
- 위시 PATCH·토너먼트 후보 수정 문서에 400 사유와 example 추가
- 단위 테스트와 위시·토너먼트 후보 통합 테스트 추가
- 결과: 검사를 빼면 통합 테스트가 200 으로 실패 (로컬 확인)
- 카드(ItemCard)가 출처 링크를 직접 가짐
- 상품이 정해지기 전: 마지막 파싱 요청의 링크
- 정해진 뒤: 상품의 정체 URL. 이미지 상품은 없음
- 이전: 카드는 요청 링크만 들고 상품 주소는 호출하는 쪽이 상품 행을 따로 읽어 붙임
- 결과: 같은 식이 위시 몰 이름·위시 sourceUrl·토너먼트 후보 sourceUrl 세 곳에 복사됨
- 원인: 상품이 생기는 시점이 파싱 성공으로 늦어져 출처가 둘로 갈렸는데 카드에는 하나만 들어감
- 카드를 만드는 곳(ItemCardService)이 상품 행을 한 번에 읽음
- 상품 행이 없으면 이 한 곳에서 error
- 잠금 읽기 카드(토너먼트 시작)는 상품 행도 FOR SHARE 로 읽음
- 원인: 버전만 최신으로 읽으면 같은 카드 안에서 두 읽기가 보는 시점이 어긋남
- 비용: 카드를 읽는 모든 곳에 상품 IN 조회 1회 추가
- 위시·토너먼트 후보 상세가 상품 행을 따로 읽던 조회 삭제
- 상품 id 와 메모는 슬롯 행의 item_id 로 읽음
- 이전: 토너먼트 후보 상세는 상품을 못 찾으면 id·링크·메모가 오류 없이 null
- 위시 결과와 사용자 입력 기록 결과에서 쓰지 않게 된 상품 칸 삭제
- 대안: 같은 식을 함수 하나로 묶기
- 기각: 호출하는 쪽이 상품 행을 따로 읽는 구조와 조용한 null 이 남음
- 단축 링크 위시가 파싱 전후로 sourceUrl·sourcePlatform 을 함께 바꾸는지 HTTP 테스트 추가
- 결과: 카드의 상품 주소를 비우면 이 테스트를 포함해 3건 실패 (로컬 확인)
- 시작 시각 칸이 생기기 전에 시작한 토너먼트의 started_at 을 추정값으로 채움
- 원인: 비어 있으면 출전 값을 지금 버전으로 계산해 새 버전마다 가격이 바뀜
- 결과: 가격 순서로 만드는 대진이 바뀌어 진행 중 경기 기록과 어긋날 수 있음
- 실제 시작 시각은 어디에도 남아 있지 않음
- 기준 시각은 출전이 가리키던 버전들 중 가장 늦은 생성 시각
- 원인: 옛 모델의 출전 값이 그 버전들이라 이 시각까지의 버전으로 계산하면 옛 값과 가장 가까움
- 가리키던 버전이 지워졌거나 다른 아이템 것이면 그 아이템의 첫 버전 시각
- 원인: 기준 시각까지 버전이 하나도 없으면 카드를 만들 수 없어 500
- 지운 출전은 계산에서 뺌
- 위험: 한 출전의 가리키던 버전 뒤, 기준 시각 전에 생긴 그 아이템의 새 버전은 값에 들어감
- 대안: 첫 경기 기록 시각
- 기각: 시작 뒤 첫 경기 전까지 생긴 새 버전까지 들어가 옛 값과 더 멀어짐
- 기각: 경기 기록이 없는 진행 중 토너먼트는 시각을 정할 수 없음
- 옛 모양 데이터에 이 SQL 을 실행하는 임시 테스트로 확인 (커밋 안 함)
- 결과: 진행 중·완료·대기·이미 채운 토너먼트와 지운 출전·지운 버전 경우 모두 기대값
- 위험: 슬롯 배포가 겹치는 동안 옛 코드가 시작한 토너먼트는 이 백필에서 빠짐
- 앞 마이그레이션의 출전 item_id 채우기도 같은 구간을 놓침
- 후속: 두 백필 모두 빈 칸만 채워 다시 돌려도 같은 결과라 다음 배포에 한 번 더 실행
- 옛 빈 아이템에 등록 원문을 채우기 전에 그 원문을 이미 정체로 가진 아이템에 합침
- 원인: 파싱 중 경합으로 같은 상품의 빈 아이템이 남으면 원문 채우기가 정체 유니크에 걸림
- 결과: 마이그레이션 실패로 새 앱이 뜨지 않아 배포가 막힘
- 정체 주인은 임시 표로 먼저 뽑아 id 로 붙임
- 원인: 원문(대소문자 무시)과 정체(바이트) 비교를 한 조인에 이으면 옵티마이저가 조건을 옮겨 같은 묶음 안에서 합칠 대상이 갈림
- 결과: 이은 조인으로는 위시·버전이 지워진 아이템을 가리킴 (로컬 실측)
- 768자를 넘는 원문은 채우지 않고 4단계 정리 대상으로 보냄
- 원인: 정체 칸이 768자라 엄격 모드에서 길이 초과로 실패
- 아직 공유 환경에 적용되지 않은 파일이라 새 파일 대신 그 자리에서 고침
- 일회용 MySQL 8.4 에 직전 마이그레이션까지 적용하고 옛 모양 데이터로 확인
- 결과: 원래 SQL 은 1062 로 실패
- 결과: 고친 SQL 은 경합·대소문자 묶음·중복 원문·긴 원문·보통 행 모두 기대값, 지워진 아이템을 가리키는 행 0
- 결과: 뒤 마이그레이션까지 이어서 성공
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review: 2
Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다. |
This branch has not been deployed
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.
한눈에
상품이 정해지는 시점을 파싱 성공으로 옮김
같은 상품을 담은 카드 합치기
Situation
파싱 성공 시점 확정
Task
파싱 성공 시점 확정
Action
아이템과 버전 확정 시점 전환
refreshing칸ItemCard)가 출처 링크까지 답함정체 URL 과 등록 판정
pid는af_파라미터가 함께 있을 때만 지움pid를 쓰는 몰reused·refreshNeeded) 삭제409 와 합치기의 경계
ItemSlotPort.fill주석같은 상품 카드 합치기
ItemSlots.fill)(범위, item_id, live)가 거부live는 삭제 시각으로 계산하는 가상 칸 (살아 있으면 1, 지우면 NULL)snapshot_id있음)은 검사에서 뺌wish_memos)로 이관사용자 입력
UserInput)과 채운 값(UserInputValues) 두 타입토너먼트
item_id인덱스 범위만 (EXPLAIN·data_locks 실측)알림·실시간 전송
ITEM_PARSING_MERGED)과 토너먼트 조용한 갱신(TOURNAMENT_ITEM_MERGED) 추가이미지·업로드
마이그레이션
정리
주요 결정
용어
ItemSlot)NormalizedInput)CanonicalInput)ItemCard)live대화에서 짚은 질문
snapshot_id칸 삭제 가능 여부Result
검증
위험
source를method로)과 상품 해시 칸 삭제후속
refreshingreused·refreshNeeded삭제item=에서outbox=로 바뀐 것에 맞춰 Grafana 알림·대시보드 확인snapshot_id·wishes.memo칸과 별칭 테이블 삭제연관 이슈