Repository navigation
받은 기록함이 서버 목록 API 를 쓰지 않고 로컬 stub 으로 동작 — record-boxes 미연동 #607
Description
2026-09-08 범위 분리·착수 조건
접근 자격 결정·타 발신자 마스터키 비노출 서버 계약 검증을 #1949로 분리했다. 아래 API/필드 표는 과거 실측 자료이며 최종 구현 계약이 아니다. 이 이슈는 #1949와 기존 BE#93의 확정 결과를 현재 로컬 목록과 대조하고 실제 필요한 연동 범위를 정한다. 미확정 응답·접근 헤더를 구현 계획으로 예약하지 않는다. 목록이 같은 이메일 기준이라는 사실만으로 안전한 사용자 격리가 성립했다고 간주하지 않는다.
📜 Overview (Required)
받은 기록함 목록이 서버 API 를 쓰지 않고 로컬 in-memory stub 으로 동작합니다. 서버에는 이미 목록 조회 API 가 있는데 FE 가 한 번도 호출하지 않습니다.
서버 계약 — GET /api/v1/receiver-auth/record-boxes (Swagger 실측, 2026-07-29)
"수신자 접근 코드로 같은 이메일에 등록된 모든 받은 기록함 목록을 조회합니다."
헤더 X-Auth-Code 가 required 고, 응답 ReceivedRecordBoxResponse 는 카드가 필요로 하는 값을 전부 내려줍니다.
| 필드 | 설명 |
|---|---|
receiverId |
수신자 ID |
accessCode |
기록 열람 시 X-Auth-Code 에 넣을 접근 코드 |
senderName |
발신자 이름 |
receiverName |
수신자 이름 |
relation |
발신자와 수신자의 관계 |
recordStatus |
받은 기록함 보관 상태 |
viewStatus |
열람 상태 |
verificationStatus |
서류 인증 상태 |
requestedAt · approvedAt |
열람 신청일 · 승인일 |
FE 현황 — record-boxes·RecordBox 전체 검색 결과 호출 0건입니다. 대신 SenderRegistry 가 카드를 메모리에 보관하고, 사용자가 마스터 키를 넣을 때마다 로컬 항목이 쌓입니다. 코드에 판단 근거가 그대로 남아 있습니다.
// 발신자 라벨 등록 API 가 미확정이라 [SenderRegistry] 로컬 stub 에 보관한다.
SenderRegistrationViewModel.kt:16 — 같은 취지의 주석이 ReceivedRecordsViewModel.kt:11 에도 있습니다("백엔드 발신자 리스트 조회 API").
즉 API 가 없다고 보고 로컬로 대체해 둔 것인데 실제로는 서버에 있었습니다. 그래서 목록이 "코드를 넣으면 서버가 주는 것"이 아니라 "코드를 넣을 때마다 로컬에 쌓이는 것"이 되어 있습니다.
이 하나가 여러 증상의 공통 원인입니다.
- 한 기기를 여러 수신자가 쓸 때 받은 기록함이 격리되지 않음 — 카드·마스터 키·본인 확인이 공유됨 #598 한 기기를 여러 수신자가 쓸 때 격리 안 됨 — 당시 record-boxes의 이메일 기준 조회만으로 격리 해결을 단정할 수 없습니다. 최종 접근 자격과 컨텍스트 정책은 security(receiver): 기록함 접근 자격·마스터키 비노출 서버 계약을 확정하고 검증한다 #1949/#1950에서 확인합니다.
착수 전 — 코드 체계 (2026-07-29 실서버 실측)
수신자 흐름에 등장하는 값은 두 가지입니다.
| 서버 필드명 | 형식 | 용도 | |
|---|---|---|---|
| 이메일 인증번호 | authCode (email/verify 요청) |
6자리 숫자 | 이메일 소유 증명 |
| 마스터 키 = 접근 코드 | authCode(발급·검증) / accessCode(조회 응답) |
UUID | X-Auth-Code 헤더에 넣는 값 |
마스터 키와 접근 코드는 같은 값입니다. 실측:
GET /users/receivers/11→authCode=59c04a15-…(발신자가 수신자에게 전달하는 마스터 키)- 그 값을 그대로
X-Auth-Code헤더에 넣어GET /receiver-auth/record-boxes호출 → HTTP 200, 목록 1건 - 응답 항목의
accessCode가 요청에 쓴 값과 동일
즉 마스터 키 하나로 "같은 이메일에 등록된 모든 받은 기록함" 목록이 나옵니다. 별도의 세션 코드를 발급받는 단계는 없습니다.
주의할 점:
-
authCode라는 이름이 6자리 이메일 인증번호와 UUID 마스터 키 양쪽에 쓰입니다. 요청 스키마를 볼 때 형식으로 구분해야 합니다(BE feat:이용자 홈 화면 UI #92·refactor: theme 관련 네이밍 변경으로 인한 리팩토링 #84 에서 사용자 노출 문구 기준으로 같은 혼동을 지적해 둔 상태). -
X-Auth-Code를 요구하는 엔드포인트는 13개(record-boxes,after-notes,diary,time-letters,message,daily-question,deep-thought,delivery-verification,presigned-url등)로, 수신자 콘텐츠 조회 전체가 이 값 하나에 걸려 있습니다. -
BE Feat/73 #74(
email/verify응답에서accessCode삭제)가 반영되면 마스터 키를 얻는 경로는 발신자가 직접 전달하는 것뿐이 됩니다. 이는 당시 응답에 대한 관찰이며, 현재 구현할 인증 헤더·권한 계약은 최종 산출물로 다시 확인합니다. -
마스터 키 하나가 그 이메일의 기록함 전체를 엽니다. 서버가 키를 이메일로 확장하기 때문입니다 —
ReceiverAuthService.getReceivedRecordBoxes()(BErelease):Receiver authenticatedReceiver = findReceiverByAuthCode(authCode); String email = normalizeEmail(authenticatedReceiver.getEmail()); List<Receiver> receivers = receiverRepository.findAllByEmailIgnoreCaseOrderByIdDesc(email);
즉 권한 근거는 키가 아니라 이메일 동일성입니다. 개별 조회(
getReceivedRecordBox)도 인증 수신자와 대상 수신자의 이메일이 다르면NOT_ENOUGH_PERMISSION을 던지는, 같은 규칙입니다.따라서 발신자 A 가 준 키로 발신자 B·C 의 기록함 목록(발신자명·관계·보관/열람 상태)까지 표시됩니다. 연동하면 이 동작이 그대로 화면에 드러나므로 feat(afternote): 수신자 본인확인 이메일 인증 stub → 실 API(receiver-auth/email) 전환 #407·수신자 본인 확인 캐시가 발신자 구분 없이 전역 저장 — 첫 인증 후 이메일 관문 무력화 #597 의 보안 논의와 함께 봐야 합니다. 콘텐츠 실제 열람은
viewStatus·delivery-verification 승인이 별도로 걸립니다. -
받은 기록함 발신자 카드가 앱 재시작 시 소실 — SenderRegistry 가 메모리 전용 #599 카드가 앱 재시작 시 소실 — 서버가 목록의 원본이 되면 로컬 영속 저장 자체가 불필요해집니다. 그 이슈에서 검토 중이던 DataStore 저장·마스터 키 평문 보관 문제도 함께 사라집니다.
실기기 재현 — 서버는 승인, 화면은 열람 불가:
📌 Child Issue(Optional)
No response
📍 Note (Optional)
- 옛 feat/544 브랜치를 구현 base로 예약하지 않습니다. 확정 계약이 준비된 시점의 최신 서버·FE 코드에서 실제 차이만 정합니다.
- #599의 재시작 복원은 확정 서버 목록 계약으로 해결되는지 검증합니다. 로컬 영속화나 서버 조회만으로 해결된다고 미리 단정하지 않습니다.
- 수신자 본인 확인 캐시가 발신자 구분 없이 전역 저장 — 첫 인증 후 이메일 관문 무력화 #597(본인 확인 캐시가 발신자 구분 없이 전역)은 이메일 인증 캐시 키 설계라 별개 층으로 남습니다. 다만 이 전환으로 "카드에 마스터 키를 얹어 두는" 구조가 없어지면 한 기기를 여러 수신자가 쓸 때 받은 기록함이 격리되지 않음 — 카드·마스터 키·본인 확인이 공유됨 #598 에 적힌 겹침 중 카드 쪽은 해소됩니다.
- 발단: 07-29 Figma 스레드에서 PM 이 "코드를 넣으면 저 목록이 나와야 하는데 지금 코드 없이 수신자가 등록되는 구조"라고 지적한 건과 같은 사안입니다. Swagger 대조 결과 그 지적이 맞았습니다.
Activity
1hyok commentedon Jul 30, 2026
확증 — record-boxes 는 화면이 필요한 값을 전부 준다
열람 승인된 대조군 수신자(receiverId=18)로 2026-07-30 실측.
GET receiver-auth/record-boxes200:{"receiverId":18,"accessCode":"bd22c849-…","senderName":"QA407Sender", "receiverName":"QA730Receiver","relation":"DAUGHTER","recordStatus":"STORED", "viewStatus":"VIEWABLE","verificationStatus":"APPROVED", "requestedAt":"2026-07-29T16:58:36","approvedAt":"2026-07-30T04:25:42"}앱 전체 흐름(받은 기록함 → 발신자 상세 → 열람)에서 이 API 는 한 번도 호출되지 않는다(logcat OkHttp 전 구간 확인).
이 미연동이 만드는 개별 증상들이 각각 이슈로 올라와 있는데, 전부 위 응답 한 번으로 해소된다.
receiverId·accessCode별 레코드approvedAtrequestedAt부수 확인 — 열람 신청 흐름에도 같은 문제가 있다. 서버가
verificationStatus: APPROVED·viewStatus: VIEWABLE인 상태에서 "열람 신청하기" 로 진입하면 앱이 이메일 인증 → 마스터 키 → 증빙 서류 업로드까지 4단계를 처음부터 다시 요구한다. 승인 상태를 조회하지 않기 때문이다. 뒤로 나갔다 상세로 재진입하면 그제서야 "열람 가능" 으로 바뀐다. 서류를 실제로 재업로드하면 새PENDING이 생겨 승인이 풀릴 수 있는 경로다.1hyok commentedon Aug 2, 2026
실서버 승인 상태에서도 화면이 서버를 전혀 보지 않는 것을 실기기로 확인
2026-08-02, Pixel_7_Claude_QA / debug 빌드(서버 인증서 검증 우회). 대조군을 새로 만들어 승인까지 끝낸 상태입니다 — 수신자
QACapReceiver(id 19), 사후 전달 조건AFTERNOTE / RECEIVER_REQUEST, 사망확인 서류id 8APPROVED.서버는 이미 열람을 허용합니다.
그런데 앱은 그대로 "열람 불가 / 신청 기록이 없습니다 / 승인 기록이 없습니다" 입니다.
결정적인 건 네트워크 로그입니다 — 앱 시작 → 받은 기록함 → 발신자 등록 → 발신자 상세까지 가는 동안 OkHttp 요청이 0건입니다. 실패해서 폴백된 게 아니라 애초에 호출하지 않습니다.
record-boxes미연동이라는 본문 진단 그대로입니다.부수 확인 — 같은 흐름에서 앱을 재시작하면 방금 만든 발신자 카드가 사라집니다(#599). 카드가 로컬 전용이라는 것과 같은 뿌리입니다.
이 stub 때문에 #617(수신 목록 카테고리 enum)·#611(403 원인 미안내)은 앱에서 재현 경로 자체가 막혀 있습니다 — 서버는 200 을 주는데 목록 화면까지 갈 수단이 없습니다. 이 이슈를 먼저 처리해야 그 둘을 확인할 수 있습니다.
1hyok commentedon Aug 25, 2026
재현 확인 (develop
4e0a648eb, 2026-08-25 15:12 KST, 서버 정상 응답 상태)발신자 등록(받은 기록함
+→ 이름 입력 → 등록)을 실행했으나 OkHttp 로그에 대응 요청이 전혀 없습니다. 마지막 네트워크 로그가 15:10:28(앱 시작 시 조회 5건)이고, 15:12 등록 시점에는 요청이 발생하지 않았습니다.결과적으로 등록분이 서버에 남지 않고 앱 재시작 시 소실됩니다(#599 에 상세 기록).
서버는 같은 시각 정상 응답 중이었습니다 —
GET /api/v1/daily-questions/today가 0.08초에 401(인증 필요)을 반환.docs(qa): 서버 복구 후 2차 실측 결과를 현황에 추가한다
1hyok commentedon Aug 25, 2026
승인 이후 화면이 이 이슈 때문에 통째로 막힌다 — 실측 (develop
4e0a648eb, 2026-08-25 18:34 KST)admin 승인 API 로 이 수신자(receiverId=14)의 사망확인을 APPROVED 로 만들어 두고, 앱에서 수신자 흐름을 다시 탔습니다. 서버·앱 status API 모두 APPROVED 를 반환하는 상태입니다.
그런데 앱은:
원인이 이 이슈다
받은 기록함이
record-boxes를 안 쓰고 로컬 stub 으로 도니까, 서버의verificationStatus=APPROVED를 읽는 경로가 없다. 그래서 카드는 항상 초기값("열람 불가")이고, 열람 신청은 서버 상태와 무관하게 인증 4단계를 처음부터 태운다.FE 인프라는 이미 다 있는데 화면이 호출만 안 한다 —
getDeliveryVerificationStatus()가 service·repository·domain 계층까지 구현돼 있으나 presentation 에서 호출부 0건이고, 테스트에error("호출되면 안 됨")·error("unused")로 미사용이 박제돼 있다. (ReceiverAuthApiService.kt:55,MasterKeyViewModelTest.kt:150)즉 이 이슈(record-boxes 연동)를 처리하면 #599(카드 소실)·#612(승인일 빈 값)뿐 아니라 승인 이후 화면 진입 자체가 함께 풀린다. 반대로 이걸 안 고치면 admin 이 승인해도 수신자는 앱에서 기록을 영영 못 본다.
관련
1 remaining item
[-]fix(afternote): 받은 기록함이 서버 목록 API 를 쓰지 않고 로컬 stub 으로 동작 — record-boxes 미연동[/-][+]받은 기록함이 서버 목록 API 를 쓰지 않고 로컬 stub 으로 동작 — record-boxes 미연동[/+]fix(receiver): 받은 기록함 응답의 근거 없는 nullable 을 걷는다
docs(receiver): recordStatus 스키마 enum 과 실제 값의 차이를 남긴다
1hyok commentedon Sep 13, 2026
서버 계약이 바뀌었다. 새 계약 인계 (2026-09-13 실측)
지금 이 이슈에서 코드로 뗄 독립 범위가 없다고 판정했다. 본문 「서버 계약」 표가 가리키는 API 가 서버에서 사라졌고, 마지막 실측 코멘트(2026-08-25)도 구 계약 기준이다. 그대로 착수하면 삭제된 엔드포인트를 구현하게 되므로, 지금 확정 가능한 값만 옮겨 적는다. #1949 가 남은 몫으로 지목한 인계가 이것이다. 코드 변경은 0줄이다.
판정 경로는 Afternote-BE
origin/main3593aa3(2026-09-09)과 Afternote-FEorigin/develop7d87aa3f4다.1. 새 서버 계약
목록 조회는
GET /api/v1/received-records다. 핸들러 인자가@UserId Long userId하나뿐이고 헤더 파라미터가 없다. 자격은 액세스 토큰이며X-Auth-Code는 계약에서 없어졌다.응답 래퍼 키는
recordBoxes그대로다(ReceivedRecordBoxListResponse.java:11). FE DTO 의 래퍼 구조는 갈아엎을 필요가 없다.항목은 아홉 필드다.
receiverId·senderName·receiverName·relation·recordStatus·viewStatus·verificationStatus·requestedAt·approvedAt. 근거는ReceivedRecordBoxResponse.java:13-70.accessCode가 없다. BEsrc전역에서 0건이다.recordStatus의 BE 타입이 String 에서 enumReceivedRecordStatus로 바뀌었다(:33). 허용값은STORED·EMPTY둘뿐이다. JSON 표현은 여전히 문자열이라 FE 파싱이 바로 깨지지는 않지만, 값 집합이 닫혔다.requestedAt·approvedAt은LocalDateTime이다(오프셋 없는 ISO-8601, Asia/Seoul).approvedAt은 인증 상태가 APPROVED 일 때만 채워진다(:99-102).상세와 하위 경로는
GET /api/v1/received-records/{receiverId}아래로 모였다(ReceivedRecordController.java:35이하). 초대 수락은POST /api/v1/receiver-invitations/{token}/accept다(ReceiverInvitationController.java:47).POST /users/receivers는 없다.UserController의 receivers 매핑은 GET(:201·:214), PATCH(:228·:242·:280), DELETE(:295) 뿐이다.목록에서 칸을 골라내는 키가 마스터 키에서
receiverId로 바뀐다. 마스터 키가 응답에서 없어졌기 때문이다.2. FE 가 죽은 계약을 가리키고 있는 자리 (
origin/develop7d87aa3f4)feature/receiver/data/src/main/kotlin/com/afternote/feature/receiver/data/service/ReceiverAuthApiService.kt:65가@GET("receiver-auth/record-boxes")다.feature/receiver/data/src/main/kotlin/com/afternote/feature/receiver/data/dto/ReceiverAuthDto.kt:132의accessCode: String이 non-null 이고:228에서masterKey = accessCode로 옮겨 담는다. 서버가 이 필드를 안 내려주는 순간 목록 파싱이 예외로 떨어진다.feature/receiver/data/src/test/kotlin/com/afternote/feature/receiver/data/dto/ReceivedRecordBoxContractTest.kt가 같은 필드를 박제해 두었다(:37·:58·:78·:99·:102·:141·:212). DTO 와 함께 갈아야 한다.feature/receiver/data/src/main/kotlin/com/afternote/feature/receiver/data/network/ReceiverAuthInterceptor.kt:32가/receiver-auth/경로에X-Auth-Code를 자동 부착한다.core/network/src/main/kotlin/com/afternote/core/network/service/UserApiService.kt:39가@POST("users/receivers")다.receiver-auth엔드포인트 선언이 14건이다(receiver 10, mindrecord 2, timeletter 2). 목록 전환 자체는 receiver·core 안에서 닫히지만,receiver-auth폐기 전체는 mindrecord·timeletter 모듈을 건드린다. 그 두 모듈분은 이 이슈의 담당 범위가 아니다.3. 본문과 옛 코멘트에서 이미 사실이 아닌 것
본문은 2026-09-08 머리말이 「최종 구현 계약이 아니다」라고 밝혀 두었으므로 본문을 고치지 않고 여기에 정정만 적는다.
GET /api/v1/receiver-auth/record-boxes와 required 헤더X-Auth-Code. BE 에receiver-auth매핑이 0건이다. 위 1번이 대체한다.accessCode(기록 열람 시X-Auth-Code에 넣을 접근 코드). 응답에 없고 BEsrc전역 0건이다.ReceiverAuthService.getReceivedRecordBoxes()자바 스니펫. 권한 근거가 이메일 동일성에서Receiver.accepted_user_id로 바뀌었고(Receiver.java:33-34, 값이 채워지는 경로는:77초대 수락 하나),ReceiverRepository에 이메일로 찾는 메서드가 없다(:15-17은findAllByUserId·findAllByAcceptedUserIdOrderByIdDesc·findByIdAndAcceptedUserId).ReceiverAuthController·ReceiverAuthService는 파일째 없어졌다.userId·acceptedUserId뿐이라 이 노출 경로가 코드에서 사라졌다. 대신 초대 수락분만 보이는 범위 문제가 아래 5번으로 남는다.X-Auth-Code를 요구하는 엔드포인트 13개」. BE 에서 그 접두 매핑이 전부 사라졌다. 남아 있는 것은 FE 쪽 선언 14건이다.receiver.auth_code)를 읽는 코드 자체가 BE 에 없다. 취득 경로가 좁아진 것이 아니라 값이 계약에서 없어진 상태다.record-boxes·RecordBox전체 검색 결과 호출 0건」.feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/senderdetail/SenderDetailViewModel.kt:179가getReceivedRecordBoxes()를 부른다(발신자 상세 '승인일' 이 승인 이후에도 빈 값 — approvedAt 을 주는 record-boxes 미사용 #612 를 닫은 fix(receiver): 발신자 상세 승인일을 record-boxes 에서 채운다 (#612) #1537 의 승인일 우회). 목록 화면만 stub 이고 호출 0건은 더 이상 사실이 아니다.feat/544만 지목한다. 아래 4번의 두 브랜치에도 같은 금지를 적용한다.4. 착수 순서와 함정
refactor/1803-receiver-mvi, Closes refactor(receiver): MVI 전환 — receiver 소유 ViewModel 7개·Screen 8개 #1803)가ReceivedRecordsViewModel.kt·SenderRegistrationViewModel.kt·SenderDetailViewModel.kt를 통째로 MVI 로 다시 쓴다. 목록 전환은 refactor(receiver): 구 수신자 흐름 MVI 전환 철회 (#1803) #2039 머지 뒤에 올린다. 지금 올리면 정면 충돌한다.origin/fix/607(7298c0fa9, 2026-09-03)과origin/fix/607-record-boxes-integration(aa618832d, 2026-08-23)이 남아 있다. 둘 다 develop 대비 2943 커밋 뒤처진 구 계약 WIP 이라 구현 base 로 쓰지 않는다.feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/recordsbox/SenderRegistry.kt:37의UUID.randomUUID()라receiverId와 대응이 없다. 목록을 서버로 옮길 때 카드 식별을 조각내 이관할 수 없고 한 번에 갈아야 한다.5. 여전히 막혀 있는 것
blocked_by 에 걸린 셋 중 열려 있는 것은 Afternote-BE#93 과 #1949 두 건이다(#544 는 닫혔다). 수신자에게 어떤 발신자가 보여야 하는지(초대 수락분만인지 자기 앞으로 온 전부인지)가 PM 답을 기다리고 있다. 화면 완료 조건은 그 답이 온 뒤 확정한다. 이 코멘트는 기록이고 blocked_by 를 풀지 않는다.
1hyok commentedon Sep 20, 2026
2026-09-20 최신 계약과 코드를 다시 대조했습니다.
FE develop
f69332d995a2e9890605b3c8075c427b2dd0cc8c의 ReceivedRecordsViewModel은 SenderRegistry.senders를 그대로 노출하고 SenderRegistry는 프로세스 메모리 목록입니다. 발견된 목록 미연동 결함은 여전히 있습니다.하지만 아래 본문의 2026-07-29 API·헤더 표는 현재 구현 계약이 아닙니다. 배포 OpenAPI HTTP 200과 BE main
7c961ec3e2eb6fe3d0d87d4ba4e2dea312fdcf7f에서 받은 기록함은/api/v1/received-records계열의 JWT 소유권을 사용하며, 옛 receiver-auth/record-boxes와 accessCode 응답을 되살려 연동할 수 없습니다. ReceiverRepository는 acceptedUserId 기준으로 목록을 조회하고 ReceiverInvitationService가 개별 초대 수락으로 그 연결을 만듭니다.BE#93의 기존 전체 목록 요구와 새 초대 방식·구 수신자 이행에 관한 결정은 아직 기록되지 않았습니다. FE#1949는 2026-09-19 결정 전 구현 예약을 정리한다는 코멘트와 함께 닫혔으며, 그것을 서버 접근 계약 승인이나 이 이슈의 구현 차단 해제로 해석하지 않습니다. #544의 종료 역시 현재 권한 계약을 확정하지 않습니다.
이 이슈는 발견된 로컬 목록 결함의 기록으로 유지합니다. 현시점에 임시 Repository·마스터키 보관·미확정 초대 전환을 새로 작성할 독립 범위는 없습니다. 접근 자격, 전체 목록 범위, 구 수신자 이행이 결정되면 당시 서버와 FE의 차이만 다시 판단합니다. 외부 요청이나 새 구현 예약 이슈는 추가하지 않았습니다.