From 928e93660a50b402b7c4f7f55803c9914b971047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Thu, 3 Sep 2026 20:19:51 +0900 Subject: [PATCH] =?UTF-8?q?refactor(core):=20=EC=88=98=EC=8B=A0=EC=9E=90?= =?UTF-8?q?=20=EA=B1=B0=EC=A0=88=20=EC=98=88=EC=99=B8=EA=B0=80=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=9B=90=EB=AC=B8=EC=9D=84=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=EC=9E=90=EB=A1=9C=20=EC=97=B4=EC=A7=80=20=EC=95=8A=EA=B2=8C=20?= =?UTF-8?q?=ED=95=9C=EB=8B=A4=20(#1855)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReceiverRequestRejectedException 의 `val userMessage` 는 프로덕션에서 읽는 곳이 0건이다. 유일한 소비처인 setting 의 toReceiverFailureMessage 는 값이 아니라 타입만 보고 로컬 리소스를 고르고, main 의 나머지 등장은 KDoc 링크뿐이었다. 그런데 이 접근자는 core:domain 의 public API 라 어느 모듈이든 서버 원문을 읽어 그대로 화면에 실을 수 있었다. CoreAuthFailure·ReceiverFailure 두 루트가 BE#92 를 근거로 세운 «서버 문구를 화면에 싣지 않는다» 규약이 주석으로만 지켜지던 셈이다. 접근자를 떼고 파라미터 이름을 serverMessage 로 바꿔 실체를 이름이 말하게 했다. 생성자 시그니처는 그대로라 호출부는 변경이 없다 — 서버 원문을 message 에서까지 걷어내는 일은 유일한 생산 지점을 #1595 가 옮기는 중이라 #934 에 남긴다. 존치 근거로 적혀 있던 «리포팅·로그 용도» 도 사실이 아니었다. ErrorReporter 는 redact() 로 message 를 타입 이름으로 갈아치운 사본만 올리므로 서버 문구는 리포팅 백엔드에 닿지 않는다. 그 서술을 실제 규약으로 고쳤다. --- .../repoimpl/ReceiverRequestFailureTest.kt | 3 ++- .../error/ReceiverRequestRejectedException.kt | 19 ++++++++++++++++--- .../viewmodel/ReceiverFailureMessage.kt | 9 ++++++--- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/core/data/src/test/java/com/afternote/core/data/repoimpl/ReceiverRequestFailureTest.kt b/core/data/src/test/java/com/afternote/core/data/repoimpl/ReceiverRequestFailureTest.kt index ae59f89b5f..92dad14d76 100644 --- a/core/data/src/test/java/com/afternote/core/data/repoimpl/ReceiverRequestFailureTest.kt +++ b/core/data/src/test/java/com/afternote/core/data/repoimpl/ReceiverRequestFailureTest.kt @@ -67,7 +67,8 @@ class ReceiverRequestFailureTest { assertTrue(result is ReceiverRequestRejectedException) val domainError = result as ReceiverRequestRejectedException - assertEquals("수신자 이메일은 필수입니다.", domainError.userMessage) + // 서버 원문은 접근자가 아니라 진단용 message 로만 남는다 — 화면 문구는 소비처가 타입만 보고 고른다. + assertEquals("수신자 이메일은 필수입니다.", domainError.message) assertSame(apiError, domainError.cause) } diff --git a/core/domain/src/main/java/com/afternote/core/domain/error/ReceiverRequestRejectedException.kt b/core/domain/src/main/java/com/afternote/core/domain/error/ReceiverRequestRejectedException.kt index 8924769955..f579d3df76 100644 --- a/core/domain/src/main/java/com/afternote/core/domain/error/ReceiverRequestRejectedException.kt +++ b/core/domain/src/main/java/com/afternote/core/domain/error/ReceiverRequestRejectedException.kt @@ -1,7 +1,20 @@ package com.afternote.core.domain.error -/** 사용자가 입력을 수정해 해결할 수 있는 수신자 등록·수정 요청 오류. */ +/** + * 사용자가 입력을 수정해 해결할 수 있는 수신자 등록·수정 요청 오류. + * + * **서버 원문을 접근자로 열지 않는다.** 서버 `message` 는 사용자 노출용이라는 규정이 없고(BE#92), + * 그건 [CoreAuthFailure] 와 수신자 흐름의 `ReceiverFailure` 루트가 이미 세운 규약이다. public + * 접근자로 열어 두면 어느 모듈이든 그 값을 그대로 화면에 실을 수 있다. 소비처는 값이 아니라 + * **타입만** 보고 로컬 리소스를 고른다(setting 의 `Throwable.toReceiverFailureMessage`). + * + * 원문은 진단용으로 `message` 에만 남는다. 리포팅 백엔드에는 닿지 않는다 — `core:common` 의 + * `ErrorReporter` 가 message 를 타입 이름으로 갈아치운 사본만 올린다. + * + * @param serverMessage 서버가 내려준 거절 사유 원문. 진단 문구로만 쓴다. + * @param cause 이 실패를 만든 인프라 예외. + */ class ReceiverRequestRejectedException( - val userMessage: String, + serverMessage: String, cause: Throwable, -) : Exception(userMessage, cause) +) : Exception(serverMessage, cause) diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/ReceiverFailureMessage.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/ReceiverFailureMessage.kt index f4538178f3..a4c5fb8b31 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/ReceiverFailureMessage.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/ReceiverFailureMessage.kt @@ -6,9 +6,12 @@ import com.afternote.core.ui.UiText import com.afternote.feature.setting.presentation.R /** - * [ReceiverRequestRejectedException.userMessage] 는 서버 원문이라 사용자 화면에 직접 노출하지 - * 않는다(리포팅·로그 용도로만 예외에 남긴다) — feature/receiver 의 ReceiverFailureTranslation.kt - * 이 확정한 선례(#1339)와 같은 규칙이다. + * [ReceiverRequestRejectedException] 은 **타입만** 보고 문구를 고른다. 그 예외가 나르는 서버 원문은 + * 사용자 화면에 노출하지 않으므로(BE#92 — 서버 `message` 는 사용자 노출용이라는 규정이 없다), 표시 + * 문구는 여기 로컬 리소스가 갖는다 — feature/receiver 의 ReceiverFailureTranslation.kt 이 확정한 + * 선례(#1339)와 같은 규칙이다. + * + * 그래서 그 예외는 원문을 읽을 접근자를 두지 않는다. 값은 진단용 `message` 에만 남는다. */ internal fun Throwable.toReceiverFailureMessage( @StringRes fallbackResId: Int,