Skip to content

파싱 완료 알림을 제목=아이템 이름 / 본문=상태 문구로 분리 - #924

Merged
sevineleven merged 9 commits into
devfrom
feat/913-parsing-notification-title-body-split
Aug 13, 2026
Merged

파싱 완료 알림을 제목=아이템 이름 / 본문=상태 문구로 분리#924
sevineleven merged 9 commits into
devfrom
feat/913-parsing-notification-title-body-split

Conversation

@sevineleven

@sevineleven sevineleven commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Situation

  • 파싱 완료 알림이 지금은 한 줄짜리다. 제목에 {상품명} 파싱이 완료되었어요 를 다 담고 본문은 비어 있다.
  • OS 푸시 알림의 제목은 줄바꿈 없이 한 줄에서 뒤가 잘린다. 이름과 상태를 한 줄에 이어 붙이면 이름이 길 때 정작 "무슨 일이 일어났는지"가 통째로 사라진다.
  • 그래서 지금 코드는 상품명을 10글자로 잘라 상태 문구가 살아남게 막고 있었다. 길이를 줄이려던 게 아니라 잘리는 자리를 고르려던 것이고, 그 대가로 실제 상품명 대부분이 일찍 잘렸다.
  • 논의 중에 또 하나가 드러났다. 파싱 완료 문구는 상품을 어디에 등록했든 같은데, 토너먼트에 상품을 직접 올려도 그 상품이 내 위시리스트에 들어가지는 않는다 (TournamentItemServiceWish 행을 만들지 않는다). 두 경로가 다른 사건인데 같은 문구를 쓰고 있었다.

Task

  • 파싱 완료 알림을 제목 = 상품명 / 본문 = 상태 문구 구조로 바꾼다.
  • 본문 문구를 등록 출처(위시 / 토너먼트)에 따라 나눈다.
  • 두 알림의 절단 요구가 달라진 만큼, 상품명 다듬기를 공유 가능한 형태로 정리한다.

Action

왜 제목에 이름만 두면 절단이 사라지나

제목이 이름 하나뿐이면 OS 가 뒤를 잘라도 잃는 정보가 없다. 무슨 일인지는 본문이 들고 있고, 본문은 두 줄까지 보이며 펼치면 전부 보인다. 그래서 파싱 완료 경로의 표시 글자 절단을 없앴다.

이전 이후
제목 {10글자로 자른 이름} 파싱이 완료되었어요 {이름} (절단 없음)
본문 빈 문자열 {상태 문구}
이름이 길 때 상태 문구가 잘려 사라질 위험 이름만 잘림, 무슨 일인지는 남음

절단을 없애도 글자 수 안전망은 남긴다. 조합 부호를 쌓으면 눈에 보이는 글자 1개가 수백 char 이 될 수 있고, 그대로 두면 알림 엔티티의 불변식(require(title.length <= 255))에 걸린다. 그 예외는 dispatcher 가 삼켜서 알림이 전 수신자에게 조용히 누락된다.

본문 문구를 출처로 나눈 이유

등록 경로 위시리스트에 들어가나 본문 문구
위시리스트에 직접 담기 들어간다 위시 저장이 성공했어요
토너먼트에 직접 올리기 안 들어간다 아이템이 등록됐어요

출처 판정은 새로 만들지 않았다. 알림 라우팅이 이미 "그 버전을 pin 한 출전이 있나"로 같은 판정을 하고 있어서, 그 결과를 그대로 재사용한다. 조회가 늘지 않는다.

실패 알림은 손대지 않았다. 실패한 버전은 이름이 비어 있어(추출 자체가 실패) 제목에 넣을 이름이 없다. 논의 중 성공과 같은 모양({이름} 파싱이 실패했어요)도 검토했지만, 이름이 없어 기본값 상품 으로 채워지면 "상품 파싱이 실패했어요" 라는 어색한 문구가 된다. 현행 유지.

문장을 통째로 변수에 담은 트레이드오프

방식 결과
채택 본문 템플릿을 ${completionMessage} 한 변수로 두고 문장을 통째로 채움 출처 분기가 가능해짐. 대신 이 문구는 백오피스에서 편집 불가
미채택 공통 뼈대 + 부분 변수 ({X}이 성공했어요 식) 두 문장이 구조부터 달라 공통 뼈대가 안 나옴
미채택 출처별로 템플릿 행을 따로 둠 템플릿 테이블이 타입당 한 행(PK=type)이라 자리가 없음. 스키마 변경 필요

제목 템플릿(${itemName})은 여전히 백오피스가 소유한다. 편집 능력을 잃는 건 본문 한 문구뿐이고, 이 사실을 변수 카탈로그에 주석으로 남겼다.

함께 고친 기존 버그

아이템 삭제 알림에 절단 캡이 없었다. 이 알림은 제목이 {닉네임}님이 '{이름}'을(를) 삭제했어요 라 이름 뒤에 문장이 붙는다. 상품명은 512자까지 허용되는데 캡이 없어, 긴 이름이 들어오면 위에 적은 그 불변식에 걸려 삭제 알림이 전 수신자에게 조용히 누락됐다. 캡 10글자를 적용했다.

이 발견이 절단 유틸을 공유하되 캡을 인자로 받게 만든 이유다. 두 알림의 요구가 정반대라 캡을 고정하면 한쪽이 틀린다.

알림
파싱 완료 없음 제목이 이름뿐이라 OS 절단으로 충분
아이템 삭제 10글자 이름 뒤에 문장이 붙어 우리가 자리를 지켜야 함

문서

  • 알림 목록 API 설명에서 "본문은 전 타입 빈 문자열" 서술을 폐기하고, 파싱 완료만 제목/본문이 나뉜다는 점과 출처별 문구를 반영했다.
  • example 의 상태 문구는 리터럴로 박지 않고 핸들러 상수를 끌어왔다. 문구가 바뀌면 example 이 따라오고, 상수명이 바뀌면 컴파일로 드러난다 (kind 를 파생시키는 것과 같은 결).

Result

  • 머지 전에 클라 작업(feat: 알림 카드가 body 도 표시하도록 - 파싱 완료 알림 문구 구조 변경 대응 client#460)이 선행하거나 동반돼야 한다. 지금 알림 카드는 제목만 렌더하므로, 서버만 먼저 나가면 카드에 상품명만 뜨고 무슨 일인지 안 보인다.
  • API 계약은 안 바뀌었다 (본문 필드는 이미 응답에 있고 지금은 빈 문자열). 그래서 클라를 먼저 배포해도 안전하다. 오히려 클라가 먼저 나가면 "상품명만 뜨는" 구간이 아예 생기지 않는다.
  • 마이그레이션은 템플릿 행 갱신뿐이라 스키마 변경이 없다. 되돌리려면 반대 방향 갱신 하나면 된다.
  • 절단 유틸의 계약은 절단 없음/있음 양쪽을 다 고정했다. 이모지 surrogate pair, ZWJ 로 이어진 가족 이모지, 조합 부호 폭탄, 앞뒤 공백이 글자 예산을 먹는 경우까지 포함한다. 앞의 둘은 UTF-16 이나 코드포인트 기준으로 자르면 깨지는 자리라, 경계 판정이 바뀌면 여기서 드러난다.

연관 이슈

Summary by CodeRabbit

  • 개선 사항

    • 아이템 파싱 완료 알림 제목에 상품명이 표시되고, 본문에 완료 상태가 안내됩니다.
    • 위시 저장과 토너먼트 등록 알림에 동일한 완료 문구가 적용됩니다.
    • 상품명이 없거나 공백·개행이 포함된 경우에도 읽기 쉽게 표시됩니다.
    • 긴 상품명과 이모지·조합 문자를 안전하게 처리하며, 삭제 알림의 상품명 표시도 개선했습니다.
  • 문서

    • 알림 히스토리 API 및 예시가 변경된 제목·본문 형식을 반영합니다.

- OS 푸시 제목은 줄바꿈 없이 뒤가 잘린다. 이름과 상태를 한 줄에 담으면 이름이 길 때 정작 무슨 일인지가 사라져서, 지금은 이름을 10글자로 잘라 막고 있었다 - 길이를 줄이려는 게 아니라 잘리는 자리를 고르려는 것이었고, 대가로 실제 상품명 대부분이 일찍 잘렸다
- 이름을 제목에, 상태를 본문에 둔다. 제목이 이름뿐이면 OS 가 잘라도 잃는 게 없고 body 는 두 줄까지 보인다. 그래서 파싱 완료의 표시 글자 절단을 없앴다(char 안전망만 유지)
- body 문구는 등록 출처로 갈린다 - 위시에 직접 담은 것과 토너먼트에 직접 올린 것은 다른 사건이다(토너먼트에 올려도 위시리스트에 안 들어간다). 판정은 라우팅이 이미 하고 있어 그대로 재사용했고 새 조회가 없다
- 문장을 통째로 변수로 채운다. notification_templates 가 타입당 한 행(PK=type)이라 위시용·토너먼트용 body 를 따로 둘 자리가 없고, 두 문장이 구조도 달라 공통 뼈대 + 변수로도 안 쪼개진다. 대가로 이 body 는 백오피스(#252)에서 편집할 수 없다 - title 은 여전히 템플릿이 소유한다
- 절단 유틸을 ItemDisplayName 으로 공유하고 캡을 인자로 받게 했다. 두 알림의 요구가 다르다: 파싱 완료는 제목이 이름뿐이라 절단 없음, 아이템 삭제는 "OO님이 '{이름}'을(를) 삭제했어요" 라 이름 뒤에 문장이 붙어 캡이 필요하다
- TournamentItemDeletedHandler 의 절단 누락도 함께 고쳤다. 상품명이 512자까지 허용되는데 캡이 없어, 긴 이름이 엔티티 불변식(require(title.length <= 255))에 걸리면 dispatcher 의 runCatching 이 예외를 삼켜 삭제 알림이 전 수신자에게 조용히 누락됐다
- 문서·example 갱신: "body 는 전 타입 빈 문자열" 서술 폐기, 출처별 문구 설명 추가. example 의 상태 문구는 리터럴 대신 핸들러 상수를 끌어와 문구가 바뀌면 따라오게 했다
- 테스트: ItemDisplayNameTest 로 이관하며 절단 없음/있음 양쪽 계약을 고정(이모지·ZWJ·조합부호·공백 정규화), 출처별 completionMessage 통합테스트 2건 추가

클라 대응(TeamPiKi/client#460)이 선행 또는 동반돼야 한다 - 지금 카드는 title 만 렌더해서 서버만 먼저 나가면 상품명만 보인다. API 계약은 안 바뀌어(body 필드 이미 존재) 클라를 먼저 배포해도 안전하다.
@sevineleven sevineleven added the feat 외부 가시적 새 기능 label Aug 11, 2026
@sevineleven sevineleven self-assigned this Aug 11, 2026
@github-actions

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

파싱 완료 알림의 제목을 아이템 이름으로 분리하고, 본문에 고정 완료 문구를 설정했습니다. 공통 아이템 표시명 처리기를 추가하고 삭제 알림에 적용했습니다. 템플릿 마이그레이션, API 문서, 통합 테스트를 갱신했습니다.

Changes

알림 템플릿 분리

Layer / File(s) Summary
아이템 표시명 공통 처리
src/main/kotlin/com/depromeet/piki/notification/handler/ItemDisplayName.kt, src/main/kotlin/com/depromeet/piki/notification/handler/ItemParsingCompletedHandler.kt, src/main/kotlin/com/depromeet/piki/notification/handler/TournamentItemDeletedHandler.kt, src/test/kotlin/com/depromeet/piki/notification/handler/*
ItemDisplayName이 공백과 grapheme 경계를 처리합니다. 빈 이름에는 "상품"을 사용합니다. 파싱 완료 알림은 이름을 별도 표시명으로 변환하고, 삭제 알림은 10글자로 제한합니다. 관련 단위·통합 테스트를 추가했습니다.
파싱 완료 알림 템플릿 및 검증
src/main/kotlin/com/depromeet/piki/notification/handler/ItemParsingCompletedHandler.kt, src/main/kotlin/com/depromeet/piki/notification/service/NotificationTemplateVariables.kt, src/main/kotlin/db/migration/V20260811010101__split_item_parsing_completed_template_title_body.kt, src/test/kotlin/com/depromeet/piki/notification/handler/NotificationEventHandlerIntegrationTest.kt
ITEM_PARSING_COMPLETED 템플릿의 제목을 ${itemName}으로 변경하고 본문에 "파싱이 완료되었어요"를 설정합니다. 핸들러 변수에는 itemName만 유지합니다.
API 문서 및 예시 갱신
src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt, src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApiExamples.kt
파싱 완료 알림의 제목·본문 분리를 문서화합니다. 위시와 토너먼트 예시의 본문을 공통 완료 문구로 변경합니다.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
파싱 완료 알림을 아이템 이름 제목과 출처별 상태 문구 본문으로 분리 [#913] 본문이 위시의 "위시 저장이 성공했어요"와 토너먼트의 "아이템이 등록됐어요"로 분기되지 않고 "파싱이 완료되었어요"로 고정되어 있습니다.
10글자 표시 제한을 제거하고 문자 안전 처리를 유지 [#913]
토너먼트 삭제 알림에 공통 표시명 처리 적용 [#913]
알림 카드에서 body를 렌더링하도록 클라이언트 변경 [#913] 제공된 변경에 클라이언트 NotificationItem.tsx 수정이 없습니다.

Sequence Diagram(s)

sequenceDiagram
  participant ParsingEvent
  participant ItemParsingCompletedHandler
  participant NotificationTemplate
  ParsingEvent->>ItemParsingCompletedHandler: parsing completed event
  ItemParsingCompletedHandler->>NotificationTemplate: itemName
  NotificationTemplate-->>ParsingEvent: title itemName and fixed body message
Loading
🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/913-parsing-notification-title-body-split

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/main/kotlin/com/depromeet/piki/notification/handler/TournamentItemDeletedHandler.kt (1)

34-35: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

삭제 알림 handler의 회귀 테스트를 추가하세요.

제공된 테스트는 파싱 완료 알림의 completionMessage만 검증합니다. TournamentItemDeletedHandler.resolveActorContext가 10 grapheme으로 절단된 값을 itemName에 넣는지는 검증하지 않습니다. 긴 상품명, 이모지 또는 결합 문자가 포함된 상품명, 누락된 snapshot을 각각 사용해 context.variables["itemName"]을 검증하세요.

As per path instructions: 핵심 비즈니스 규칙, 예외 케이스, 경계값 검증을 우선합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/kotlin/com/depromeet/piki/notification/handler/TournamentItemDeletedHandler.kt`
around lines 34 - 35, 파싱 완료 알림만 검증하는 테스트에
TournamentItemDeletedHandler.resolveActorContext의 itemName 회귀 테스트를 추가하세요. 긴 상품명은
10 grapheme으로 절단되는지, 이모지와 결합 문자가 포함된 이름은 grapheme 단위로 보존되는지, snapshot이 없으면 해당
기본값이 사용되는지를 각각 검증하고 context.variables["itemName"]을 확인하세요.

Source: Path instructions

src/main/kotlin/db/migration/V20260811010101__split_item_parsing_completed_template_title_body.kt (1)

27-29: 🗄️ Data Integrity & Integration | 🔵 Trivial

클라이언트가 body를 렌더링한 뒤 마이그레이션을 배포하세요.

현재 production, staging, devNotificationContentnotification.title만 표시합니다. 이 마이그레이션 후에는 제목이 아이템 이름만 포함하므로, 클라이언트 배포 전에 적용하면 파싱 완료 상태가 사라집니다. 클라이언트에서 notification.body를 함께 표시하거나, 호환 기간에는 기존 완료 문구를 제목에도 유지하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/kotlin/db/migration/V20260811010101__split_item_parsing_completed_template_title_body.kt`
around lines 27 - 29, Update the migration’s notification template around
statement.setString(1) and statement.setString(2) so clients remain compatible:
either ensure production, staging, and dev clients render notification.body
before applying this migration, or preserve the existing completion text in the
title during the compatibility period. Do not deploy the title-only template
until the client rendering path supports body.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt`:
- Around line 36-38: Update the API documentation text in NotificationHistoryApi
so it no longer states that every body except ITEM_PARSING_COMPLETED is empty.
Document that ITEM_PARSING_COMPLETED uses a source-specific status message in
body, while ANNOUNCEMENT provides the administrator-entered announcement content
in body; preserve the existing title behavior and client handling guidance.

In
`@src/main/kotlin/com/depromeet/piki/notification/handler/ItemParsingCompletedHandler.kt`:
- Around line 40-42: ItemParsingCompletedHandler의 수신자 처리에서 단일 resolveRouting 및
completionMessageOf 결과를 전체 수신자에게 재사용하지 마세요. 각 수신자에 대한 userId와
NotificationRouting을 함께 구성한 뒤 라우팅별로 그룹화하고, 그룹별로 해당 본문과 알림을 생성해 위시 수신자에게 위시
라우팅·문구가, 토너먼트 등록자에게 토너먼트 라우팅·문구가 전달되도록 수정하세요. 동일한 snapshotId를 공유하는 위시와 토너먼트의
dispatch 통합 테스트에서 수신자별 body와 routing을 검증하세요.

---

Nitpick comments:
In
`@src/main/kotlin/com/depromeet/piki/notification/handler/TournamentItemDeletedHandler.kt`:
- Around line 34-35: 파싱 완료 알림만 검증하는 테스트에
TournamentItemDeletedHandler.resolveActorContext의 itemName 회귀 테스트를 추가하세요. 긴 상품명은
10 grapheme으로 절단되는지, 이모지와 결합 문자가 포함된 이름은 grapheme 단위로 보존되는지, snapshot이 없으면 해당
기본값이 사용되는지를 각각 검증하고 context.variables["itemName"]을 확인하세요.

In
`@src/main/kotlin/db/migration/V20260811010101__split_item_parsing_completed_template_title_body.kt`:
- Around line 27-29: Update the migration’s notification template around
statement.setString(1) and statement.setString(2) so clients remain compatible:
either ensure production, staging, and dev clients render notification.body
before applying this migration, or preserve the existing completion text in the
title during the compatibility period. Do not deploy the title-only template
until the client rendering path supports body.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b889c20-a872-4178-88de-980d3f6d12aa

📥 Commits

Reviewing files that changed from the base of the PR and between ba55467 and 546b10e.

📒 Files selected for processing (10)
  • src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt
  • src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApiExamples.kt
  • src/main/kotlin/com/depromeet/piki/notification/handler/ItemDisplayName.kt
  • src/main/kotlin/com/depromeet/piki/notification/handler/ItemParsingCompletedHandler.kt
  • src/main/kotlin/com/depromeet/piki/notification/handler/TournamentItemDeletedHandler.kt
  • src/main/kotlin/com/depromeet/piki/notification/service/NotificationTemplateVariables.kt
  • src/main/kotlin/db/migration/V20260811010101__split_item_parsing_completed_template_title_body.kt
  • src/test/kotlin/com/depromeet/piki/notification/handler/ItemDisplayNameTest.kt
  • src/test/kotlin/com/depromeet/piki/notification/handler/ItemParsingCompletedHandlerTest.kt
  • src/test/kotlin/com/depromeet/piki/notification/handler/NotificationEventHandlerIntegrationTest.kt
💤 Files with no reviewable changes (1)
  • src/test/kotlin/com/depromeet/piki/notification/handler/ItemParsingCompletedHandlerTest.kt

@github-actions
github-actions Bot requested a review from m-a-king August 11, 2026 04:55
CodeRabbit 리뷰 대응.

- 문서가 "body 는 ITEM_PARSING_COMPLETED 만 값이 있다" 고 단정했는데
  ANNOUNCEMENT 도 body 변수를 갖는다(관리자가 입력한 공지 본문). 클라가 공지
  본문을 숨기거나 잘못 처리할 수 있어 두 타입을 함께 명시한다.
- TournamentItemDeletedHandler 가 이번에 도입한 10자 절단은 ItemDisplayName
  단위 테스트가 규칙을 망라하지만, 핸들러가 그 규칙에 이름을 실제로 통과시키는지는
  검증되지 않았다. 긴 이름·이모지 두 케이스로 위임을 고정한다.

이모지 케이스는 입력 설계에 두 가지를 반영했다.
- 단순 이모지(1 grapheme = 2 char)를 써서 절단 결과가 char 안전망(MAX_CHARS) 아래에
  남게 했다. 가족 이모지처럼 1 grapheme 이 11 char 인 입력은 안전망에 걸리는데,
  그 안전망의 코드 유닛 절단은 "조합 부호를 쌓은 비정상 입력에서 글자 깨짐보다
  알림 누락 방지를 택한다" 는 의도된 트레이드오프라 검증 대상이 아니다.
- 앞에 1 char 를 둬 절단 경계를 홀수로 밀었다. 이모지만 있으면 코드 유닛으로 잘라도
  짝이 맞아떨어져 회귀가 드러나지 않는다. grapheme 절단을 코드 유닛 절단으로
  바꾸면 실패하는 것을 실측 확인했다.
@sevineleven

Copy link
Copy Markdown
Collaborator Author

CodeRabbit nitpick 2건 처리

① 삭제 핸들러 회귀 테스트 — 반영 (1773031)

TournamentItemDeletedHandler 가 이번에 도입한 10자 절단은 ItemDisplayNameTest 가 규칙을 망라하지만, 핸들러가 그 규칙에 이름을 실제로 통과시키는지는 검증되지 않았습니다. NotificationRecipientResolutionIntegrationTest 에 긴 이름·이모지 두 케이스를 추가했습니다.

이모지 케이스는 입력 설계에 두 가지가 필요했습니다.

  • 단순 이모지(1 grapheme = 2 char)를 씁니다. 가족 이모지(👨‍👩‍👧‍👦)처럼 1 grapheme 이 11 char 인 입력은 절단 결과가 MAX_CHARS(100) 안전망에 걸리는데, 그 안전망의 코드 유닛 절단은 "조합 부호를 쌓은 비정상 입력에서 글자 깨짐보다 알림 누락 방지를 택한다" 는 의도된 트레이드오프라 검증 대상이 아닙니다. 처음에 가족 이모지로 썼다가 이 의도된 동작을 위반으로 잡아 실패했습니다.
  • 앞에 1 char 를 둬 절단 경계를 홀수로 밀었습니다. 이모지만 있으면 코드 유닛으로 잘라도 짝이 맞아떨어져 회귀가 드러나지 않습니다.

ItemDisplayName 의 grapheme 절단을 코드 유닛 절단으로 바꾸면 이 테스트만 실패하는 것을 실측 확인했습니다.

② 마이그레이션 배포 순서 — 지적이 맞습니다. 코드가 아니라 배포 순서로 처리합니다

클라를 확인해보니 실제로 body 를 그리지 않습니다.

apps/web/src/app/notification/_components/NotificationContent.tsx:102    message={notification.title}
apps/web/src/app/notification/_components/NotificationItem.tsx:21        <p ...>{message}</p>

이 마이그레이션이 title 을 아이템 이름만으로 바꾸므로, 클라 배포 전에 적용하면 사용자는 상품명만 보고 파싱 성공/실패를 구분할 수 없게 됩니다.

코드로 막을 수 있는 문제가 아니라 배포 순서 합의 사항이라 여기 남깁니다.

배포 순서: 클라가 body 를 렌더하도록 배포 → 그다음 이 PR 머지.

호환 기간을 두려면 마이그레이션의 title 에 기존 완료 문구를 유지하는 방법도 있지만, 그러면 이 PR 의 목적(제목 절단 시 상태 문구가 사라지는 문제)이 그대로 남습니다. 클라 선배포가 맞다고 봅니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/test/kotlin/com/depromeet/piki/notification/handler/NotificationRecipientResolutionIntegrationTest.kt`:
- Around line 147-149: Update the deletion-notification assertions around
itemName and ItemDisplayName to assertEquals the exact contract: 10 grapheme
units with the expected ellipsis handling, rather than only checking prefix and
shorter length. In the surrogate-pair test, extract the input into sourceName
and assert that the resolved itemName is shorter than sourceName while retaining
the existing surrogate-integrity checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 95ec1c44-9614-4146-8d38-e6ca6b919bce

📥 Commits

Reviewing files that changed from the base of the PR and between 546b10e and 1773031.

📒 Files selected for processing (2)
  • src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt
  • src/test/kotlin/com/depromeet/piki/notification/handler/NotificationRecipientResolutionIntegrationTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt

출처별로 문구를 가르려 했는데(위시 "위시 저장이 성공했어요" / 토너먼트 "아이템이 등록됐어요"), 지금 구조에선 위시 수신자에게 토너먼트 문구가 갈 수 있다.

dispatcher 는 라우팅과 title/body 렌더를 수신자 루프 밖에서 한 번만 해석해 전 수신자에게 같은 값을 박는다. 그런데 한 snapshot 에 서로 다른 출처의 수신자가 함께 붙을 수 있다 - 공유 정체성(#825)의 "진행 중 합류" 경로다. A 가 URL 을 위시에 담아 파싱이 도는 중에 B 가 같은 URL 을 토너먼트에 올리면, resolveAttachment 가 그 진행 중 snapshot 을 그대로 물려줘 tournament_item(B) 와 wish(A) 가 같은 버전을 가리킨다. 파싱이 끝나면 수신자는 둘인데 라우팅은 firstOrNull 이 고른 토너먼트 하나라, A 가 토너먼트 문구를 받는다.

딥링크가 어긋나는 것 자체는 이 PR 이전부터 있었고 resolveRouting 주석이 알면서 수용한다고 적어 뒀다. 다만 문구까지 그 라우팅에서 파생시키면 이 PR 이 "거짓 문구" 를 새로 만드는 셈이라, 분기를 걷어내고 단일 문구로 되돌린다.

- body_template 을 변수 없는 고정 문구("파싱이 완료되었어요")로 둔다. 부수 효과로 백오피스(#252) 편집 손실이 사라졌다 - 문장을 통째로 변수에 담느라 잃었던 것이라, 분기를 접으니 title·body 둘 다 다시 템플릿이 온전히 소유한다
- 변수 카탈로그에서 completionMessage 를 제거하고 itemName 만 남긴다
- example 의 두 파싱 항목이 같은 body 를 쓰는 게 계약이라 상수로 묶고, 문구 소유자가 DB 템플릿임을 주석으로 남긴다 (referenceItem 의 title 과 같은 방식)
- 회귀 가드: 출전 pin 이 있어도 문구 변수가 itemName 하나뿐임을 단언한다. 수신자별 해석 없이 분기를 되살리면 여기서 깨진다

출처별 문구는 수신자별 라우팅 해석·wishId 딥링크와 함께 #933 에서 다룬다.

삭제 알림 테스트 단언도 함께 조였다. "짧아졌나" 와 "짝 잃은 surrogate 가 없나" 만 보고 있어서, 핸들러가 캡을 10 대신 20 으로 바꾸거나 이모지 이름을 아예 안 잘라도 통과했다. ItemDisplayNameTest 는 캡을 인자로 받아 검증하므로 핸들러가 고른 캡 값은 이 테스트에서만 고정된다. 정확한 기대값으로 못 박았다.
…tle-body-split' into feat/913-parsing-notification-title-body-split
@sevineleven

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review body 의 nitpick 2건도 확인했습니다.

이미 반영됨 - 삭제 알림 핸들러 회귀 테스트

1773031 에 이미 들어가 있습니다. NotificationRecipientResolutionIntegrationTest 에 요청하신 세 케이스가 그대로 있습니다.

  • 토너먼트 아이템 삭제 변수의 itemName 은 표시 길이로 잘린다
  • 토너먼트 아이템 삭제 변수의 itemName 은 이모지를 쪼개지 않는다
  • 토너먼트 아이템 삭제 변수 itemName 은 상품명이 아직 없으면 fallback 이다

이 nitpick 은 그 커밋 이전 상태(8c8a8bf)를 보고 남기신 것 같습니다. 실제로 나중 리뷰의 인라인 thread 가 바로 그 테스트들의 단언이 느슨하다고 지적하셨고, 그건 방금 28dcb3a 로 조였습니다.

정정 필요 - 배포 순서

결론(클라 먼저)에는 동의하지만 영향 범위 서술이 실제보다 넓습니다. 클라 코드를 직접 확인했습니다.

전달 면 지금 body 를 쓰나 이 마이그레이션을 먼저 적용하면
FCM 푸시 OS 가 title·body 네이티브 렌더 (FirebaseMessageSender.kt:132-133) 개선
SSE 실시간 토스트 쓴다 (useNotificationSSE.ts:156, description: payload.body) 개선
알림 히스토리 카드 안 쓴다 (NotificationContent.tsx:59, message={notification.title}) 일시 퇴행

즉 "파싱 완료 상태가 사라집니다" 는 히스토리 카드 한 곳에 한정됩니다. 푸시와 토스트는 클라 수정 없이 바로 두 줄이 되며, 이게 이 PR 의 본래 목적입니다.

그래서 "호환 기간에 제목에 완료 문구를 유지" 하는 제안은 채택하지 않습니다. 그렇게 하면 이 PR 이 고치려는 문제(제목 한 줄에 이름과 상태가 함께 들어가 이름이 길면 상태가 잘려 사라짐)가 그대로 남고, 이름 절단(10글자)도 되살려야 합니다. 퇴행이 실시간 경로가 아니라 되짚기 경로 한 곳이고 레이아웃·딥링크는 그대로라, 그 대가를 치를 만큼은 아니라고 봤습니다.

클라 대응은 TeamPiKi/client#460 으로 별도 추적 중이며, 이 PR 머지 시점은 그쪽 진행에 맞춥니다.

한 가지 단서: 위 클라 확인은 로컬 dev 체크아웃(d60e67d) 기준이라 원격보다 뒤처져 있을 수 있습니다. 히스토리 카드가 그 사이 바뀌었다면 퇴행 항목 자체가 사라집니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt (1)

36-39: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

body 렌더링 클라이언트 배포를 릴리스 조건으로 고정하세요.

현재 클라이언트가 title만 렌더링하면 서버가 body에 저장한 "파싱이 완료되었어요"가 사용자에게 표시되지 않습니다. TeamPiKi/client#460을 선행 또는 동반 배포하고, 파싱 완료 알림 카드가 비어 있지 않은 body를 표시하는 검증을 릴리스 체크리스트에 추가하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt`
around lines 36 - 39, NotificationHistoryApi의 알림 body 계약 변경과 함께 body를 렌더링하는
클라이언트 변경(TeamPiKi/client#460)을 선행 또는 동반 배포로 고정하세요. 파싱 완료 알림 카드가 비어 있지 않은 body,
특히 “파싱이 완료되었어요”를 표시하는지 릴리스 체크리스트의 검증 항목에 추가하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt`:
- Around line 36-39: NotificationHistoryApi의 알림 body 계약 변경과 함께 body를 렌더링하는 클라이언트
변경(TeamPiKi/client#460)을 선행 또는 동반 배포로 고정하세요. 파싱 완료 알림 카드가 비어 있지 않은 body, 특히 “파싱이
완료되었어요”를 표시하는지 릴리스 체크리스트의 검증 항목에 추가하세요.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: c1663dbd-94e5-463f-be51-8391e578d558

📥 Commits

Reviewing files that changed from the base of the PR and between 1773031 and d78b47b.

📒 Files selected for processing (7)
  • src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt
  • src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApiExamples.kt
  • src/main/kotlin/com/depromeet/piki/notification/handler/ItemParsingCompletedHandler.kt
  • src/main/kotlin/com/depromeet/piki/notification/service/NotificationTemplateVariables.kt
  • src/main/kotlin/db/migration/V20260811010101__split_item_parsing_completed_template_title_body.kt
  • src/test/kotlin/com/depromeet/piki/notification/handler/NotificationEventHandlerIntegrationTest.kt
  • src/test/kotlin/com/depromeet/piki/notification/handler/NotificationRecipientResolutionIntegrationTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/kotlin/com/depromeet/piki/notification/handler/NotificationRecipientResolutionIntegrationTest.kt
  • src/main/kotlin/db/migration/V20260811010101__split_item_parsing_completed_template_title_body.kt

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@sevineleven 확인했습니다.

삭제 알림 회귀 테스트 nitpick은 177303128dcb3a로 처리되었습니다. 세 테스트는 핸들러의 ItemDisplayName 적용과 이모지 경계 보존, fallback을 검증합니다.

배포 순서에 대한 기존 설명은 영향 범위를 과도하게 넓게 표현했습니다. 정정합니다.

title에 기존 완료 문구를 유지하는 방식은 긴 상품명에서 상태 문구가 잘리는 문제를 다시 만듭니다. 따라서 클라이언트 선배포 후 이 PR을 머지하는 순서가 적절합니다. 토끼도 범위를 정확히 잡았습니다. 🐇


✏️ Learnings added
Learnt from: sevineleven
URL: https://github.com/TeamPiKi/core/pull/924

Timestamp: 2026-08-12T09:08:20.052Z
Learning: TeamPiKi/core의 `ITEM_PARSING_COMPLETED` 알림에서 FCM 푸시는 `title`과 `body`를 네이티브로 렌더링하고, 클라이언트 SSE 토스트는 `payload.body`를 렌더링한다. 알림 히스토리 카드는 TeamPiKi/client#460이 배포되기 전까지 `body`를 렌더링하지 않으므로, title/body 템플릿 전환의 일시적 호환성 영향은 히스토리 카드에 한정된다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: sevineleven
Repo: TeamPiKi/core PR: 924
File: src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt:0-0
Timestamp: 2026-08-11T10:32:22.991Z
Learning: `src/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.kt`의 알림 히스토리 API에서 `ANNOUNCEMENT` 타입은 관리자 입력 공지 본문을 `body` 필드로 제공한다. `ITEM_PARSING_COMPLETED` 타입은 아이템 이름을 `title`, 출처별 상태 문구를 `body`로 제공하며, 그 외 다수 타입의 `body`는 빈 문자열일 수 있다.

You are interacting with an AI system.

@sevineleven
sevineleven merged commit eeed118 into dev Aug 13, 2026
7 checks passed
@sevineleven
sevineleven deleted the feat/913-parsing-notification-title-body-split branch August 13, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 외부 가시적 새 기능

Projects

None yet

Development

Successfully merging this pull request may close these issues.

파싱 완료 알림을 제목=아이템 이름 / 본문=상태 문구 구조로 분리

1 participant