You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
추억 노트 미디어 음성 녹음 첨부 — #369 에서 서버 계약 부재로 빠진 갈래 #1118
#369 가 추억 노트 미디어에 즉석 촬영을 붙였지만(사진·영상, PR #1112), 녹음은 서버 자리가 없어 빠졌다. 그 자리가 서면 붙이는 후속.
지금 상태:
계층
오디오
PhotoUploadRepositoryImpl.ALLOWED_EXTENSIONS
mp3·m4a·wav 이미 포함
서버 업로드 계약 (presigned-url)
허용 (mp3·m4a·wav, 상한 50MB)
MediaKind
PHOTO, VIDEO — 없음
저장 페이로드 (memorialPhotoUrl·memorialVideo)
자리 없음
즉 올릴 수는 있는데 추억 노트에 붙여 둘 필드가 없어 저장 시점에 버려진다. 서버 필드가 선행이라 blocked_by 로 걸어 둔다.
🚀 해야할 일
MediaKind 에 AUDIO 추가 — MemorialMediaUploadRepositoryImpl.resolve 의 when 이 컴파일 에러로 누락을 잡아 준다
ResolveMemorialMediaForSaveUseCase 에 오디오 갈래 추가 + 저장 페이로드 매핑
녹음 인텐트 — MediaStore.Audio.Media.RECORD_SOUND_ACTION. 사진·영상과 달리 결과 URI 를 인텐트가 돌려주는 방식이라 TakePicture/CaptureVideo 처럼 출력 URI 를 미리 넘기는 계약이 아니다. 전용 ActivityResultContract 를 짜야 한다
녹음 앱이 없는 기기 처리 — MemorialMediaSourceState.launchCapture 의 ActivityNotFoundException 갈래를 그대로 재사용
UI 자리 — 추억 노트 편집 화면에 음성 슬롯을 새로 두는지, 기존 슬롯에 합치는지 시안 확인 필요. 미답변이면 FE 재량으로 확정하고 근거를 PR 에 남긴다
시트 옵션 — MemorialMediaSourceBottomSheet 의 MemorialMediaTarget 에 AUDIO 추가 (아이콘은 타임레터 ic_mic 와 같은 모양으로 신규)
changed the title [-]feat(afternote): 추모 미디어 음성 녹음 첨부 — #369 에서 서버 계약 부재로 빠진 갈래[/-][+]feat(afternote): 추억 노트 미디어 음성 녹음 첨부 — #369 에서 서버 계약 부재로 빠진 갈래[/+]on Aug 25, 2026
changed the title [-]feat(afternote): 추억 노트 미디어 음성 녹음 첨부 — #369 에서 서버 계약 부재로 빠진 갈래[/-][+]추억 노트 미디어 음성 녹음 첨부 — #369 에서 서버 계약 부재로 빠진 갈래[/+]on Aug 28, 2026
신규 불필요 — core:ui 에 core_ui_ic_mic.xml 이 이미 있고 타임레터가 그걸 쓴다 (MediaBottomSheetContent.kt:50)
여전히 맞는 것: MediaKind 는 PHOTO·VIDEO 뿐, presigned 는 mp3·m4a·wav 허용(상한 50MB — max-bytes.audio:52428800), PhotoUploadRepositoryImpl.ALLOWED_EXTENSIONS 에 오디오 확장자 포함.
우리 S3 키만 받는다.afternotes/ 디렉터리의 관리 객체가 아니면 UNMANAGED_MEDIA_URL, 확장자가 {mp3, m4a, wav} 밖이면 INVALID_FILE_EXTENSION — 둘 다 400. 즉 업로드는 반드시 DIRECTORY_AFTERNOTES 로 가야 하고 확장자가 저 셋 중 하나로 확정돼야 한다.
PATCH 삭제가 열렸다.PlaylistRequestDeserializer 가 «필드 생략 = 유지» 와 «JSON null = 삭제(DB 참조 + S3 객체)» 를 가른다. 현재 FE 는 Json { } 기본값(encodeDefaults = false)이라 값이 null 인 필드를 아예 안 실어 보내므로 지금 동작은 «유지» 로 안전하다.
착수 전에 바뀌어야 할 항목
1. 녹음 인텐트 — RECORD_SOUND_ACTION 은 확장자를 보장하지 못한다
본문의 «MediaStore.Audio.Media.RECORD_SOUND_ACTION + 전용 ActivityResultContract» 는 계약 관점에서 위험하다. 결과 포맷을 녹음 앱이 정한다 — AOSP 계열은 audio/amr·audio/3gpp, 삼성은 audio/mp4(m4a). BE 화이트리스트는 mp3·m4a·wav 뿐이라 amr/3gp 가 오면 400 으로 튕긴다. 게다가 PhotoUploadRepositoryImpl.resolveExtension 은 모르는 MIME 을 DEFAULT_EXTENSION = "jpg" 로 떨어뜨려서, 녹음 파일이 .jpg 로 올라간 뒤 오디오 슬롯 검증에서 다시 튕긴다.
→ 인앱 MediaRecorder(MPEG_4 + AAC → .m4a)가 출력 포맷을 결정적으로 잡는 유일한 길이다. 대신 RECORD_AUDIO 런타임 권한이 필요해지고(인텐트 경로는 불필요) 매니페스트를 건드리므로 CI Test Plan 이 selected/full 로 올라간다. 인텐트 경로를 유지한다면 확장자 화이트리스트 밖 결과를 거르는 갈래가 반드시 붙어야 한다.
또한 core 에 오디오 전용 업로드 경로가 필요하다 — 지금은 PhotoUploadRepository·VideoUploadRepository 뿐이고 전자는 위처럼 오디오 MIME 을 jpg 로 흘린다.
2. UI 자리 — 시안 없음 (실측)
Figma 정리 Screen Design 의 애프터노트(4327:99446)·NEW 추억 노트(4327:72816) 두 구역 전체를 REST 로 받아 노드명·텍스트를 «음성·녹음·보이스·오디오·voice·audio·mic» 으로 전수 검색 → 실히트 0건(42건은 Dynamic Island spacer 오탐). 수신자 열람 쪽도 같다.
→ 「미답변 디자인 질의는 FE 재량」 방침대로 시안 부재는 블로커가 아니다. 기존 미디어 첨부 관례(MemorialPhotoUpload·MemorialVideoUpload 슬롯 + MemorialMediaSourceBottomSheet)를 그대로 따라 세 번째 슬롯을 두는 형태로 확정하고 근거를 PR 에 남긴다.
3. 재생 UI
수신자 열람 MemorialReceivedDetailScreen 에 영상 섹션이 이미 있고, core:common 으로 승격된 launchMemorialVideo(#1436)가 http/https 스킴만 골라 외부 앱으로 여는 패턴을 쓴다(#1394) — 실패는 스낵바 2문구로 갈린다(#1391). 오디오도 같은 함수를 그대로 태우면 새 플레이어 없이 닫힌다.
지금 착수하지 않는 이유 — 파일 경합
계약은 열렸지만 이 기능의 본체(폼 상태 + 저장 배선)가 열린 PR 4건과 같은 파일을 쓴다.
특히 #1565 가 AfternoteTypeForm.Memorial 을 값 객체로 재편하며 picked/server 축을 가르는 중이다. 그 전에 옛 모양 위에 오디오 필드를 얹으면 #1565 가 오디오까지 다시 손봐야 하고, 반대로 #1565 뒤에 붙이면 오디오도 같은 값 객체 규격을 그대로 받는다. 그 아래 계층(MediaKind·ResolveMemorialMediaForSaveUseCase·DTO·매퍼)만 먼저 내면 폼에서 값이 안 흘러 전부 휴면 코드가 된다.
→ #1565 머지 후 한 판으로 간다. 순서: MediaKind.AUDIO + core 오디오 업로드 → 폼 상태·저장 배선 → 녹음 슬롯 UI + 시트 옵션 → 수신자 재생.
갱신된 체크리스트
MediaKind 에 AUDIO 추가 — MemorialMediaUploadRepositoryImpl.resolve 의 when 이 누락을 컴파일 에러로 잡아 준다
core 오디오 업로드 경로 — PhotoUploadRepository 는 모르는 오디오 MIME 을 jpg 로 흘린다
ResolveMemorialMediaForSaveUseCase 오디오 갈래 + MemorialWritePayload.memorialAudioUrl + DTO(AfternotePlaylistRequestDto·AfternotePlaylistDto)·매퍼 양방향
녹음 수단 — 인앱 MediaRecorder(m4a 확정, RECORD_AUDIO 권한) vs RECORD_SOUND_ACTION(확장자 비보장, 화이트리스트 필터 필수) 중 택일. 기기 실측으로 확정
녹음 앱/권한 부재 처리 — MemorialMediaSourceState.launchCapture 의 ActivityNotFoundException 갈래 재사용
폼 자리 — MemorialPhotoUpload·MemorialVideoUpload 관례를 따른 세 번째 슬롯 (시안 0건, FE 재량)
시트 옵션 — MemorialMediaTarget.AUDIO (아이콘은 core_ui_ic_mic 재사용, 신규 제작 불필요)
특히 #1605 가 승격한 MediaSelectSheetContent 는 항목 개수를 계약에 박지 않았습니다(items: List<MediaSheetItem>). 그 스택이 착륙한 뒤에 시작하면 음성 항목은 호출부에서 원소 하나 더 넣는 것으로 끝나고 core:ui 는 안 건드려도 됩니다.
정리 — 이 이슈의 남은 선행 조건은 「서버 계약」이 아니라 「위 스택 착륙」입니다. blocked_by 를 손대야 하면 그 기준으로 다시 거는 게 맞습니다.
본문은 「저장 페이로드에 음성을 실을 자리가 없다」를 전제로 Afternote-BE#248 을 blocked_by 로 걸어 뒀는데, 그 블로커는 2026-08-30 에 닫혔다. BE origin/main8e605f4 실코드로 확인했다 (OpenAPI 는 전 필드를 optional 로 보여 주므로 근거로 쓰지 않았다):
본문에 함께 적힌 「PATCH 가 첨부 삭제를 표현하지 못한다」도 같은 커밋에서 풀렸다.PlaylistRequestDeserializer 가 「키 없음」과 「키 있고 JSON null」을 갈라 AfternotePlaylist.update 의 specified 플래그로 넘긴다 — 즉 명시적 null 을 보내면 DB 참조와 S3 객체가 함께 지워진다.
MediaKind.AUDIO · ResolveMemorialMediaForSaveUseCase 오디오 갈래 · 저장 페이로드 매핑 — 완료.
녹음 인텐트 — 완료. 본문 지적대로 RECORD_SOUND_ACTION 은 결과 URI 를 인텐트가 돌려주는 계약이라 전용 ActivityResultContract(RecordSoundContract)를 새로 짰다.
녹음 앱 부재 처리 — 완료. 기존 ActivityNotFoundException 갈래를 그대로 재사용.
시트 옵션 — 완료. 다만 「파일에서 선택」·「음성 녹음」 두 갈래다. 음성은 사진 선택기(PickVisualMedia) 대상이 아니라 문서 선택기(SAF)로 골라야 해서 첫 항목의 아이콘·문구도 슬롯을 따라 갈린다. 아이콘은 신규로 그리지 않고 core_ui_ic_mic(타임레터 시안과 같은 모양)을 재사용했다.
UI 자리 — FE 확정. 착수 시점에 시안을 다시 열었지만 「NEW 추억 노트」 구역(4327:72816)에 추억노트_사진추가·추억노트_영상 추가만 있고 음성 프레임이 없다. 파일 전체에서 「음성 추가하기」가 나오는 곳은 타임레터 블록 시트뿐이다. 그래서 영상 슬롯 바로 뒤에 같은 규격으로 「남기고 싶은 목소리」 섹션을 새로 뒀다 — 근거는 PR 본문에.
재생 UI — 이번 범위에서 뺐다. 영상 슬롯도 앱 안에서 재생하지 않고 첨부 여부만 보여 준다. 수신자 열람 화면의 재생 수단은 영상과 함께 다뤄야 일관돼서 뒤로 미뤘다.
아직 안 한 것
이 이슈는 열어 둔다 — PR #1625 는 Closes 가 아니라 Refs 로 걸었다. 아래 항목은 본문 체크리스트의 마지막 줄(재생 UI)이 계속 추적하며, 담당은 그대로 나(afternote·receiver 모듈)다. 남에게 넘기는 몫은 없다.
수신자 열람 화면의 음성 노출·재생. 서버 응답에는 이미 실려 오지만(위 표) FE 수신자 경로는 아직 읽지 않는다. 영상의 재생 수단과 함께 다루는 편이 낫다.
참고로 첨부 단계에서 막은 형식이 있다 — 안드로이드 기본 녹음기가 흔히 내놓는 audio/amr·audio/3gpp 는 서버가 안 받아 첨부 시점에 안내하고 되돌린다. 서버가 형식을 늘리면 MemorialAudioFormats 한 곳만 고치면 된다.
착수 시 함께 판단할 것 — 썸네일 저장 직전 재업로드를 ResolveMemorialMediaForSaveUseCase 로 모을지 (2026-09-02)
완료 조건의 「ResolveMemorialMediaForSaveUseCase 에 오디오 갈래 추가」 를 할 때, 같은 자리에 썸네일 갈래도 모을지 한 번에 결정한다. 두 번 손대지 않기 위한 메모다.
현황.#1550 (PR #1567) 이 추모 영상 썸네일 실패에 재시도를 붙이면서, 저장 직전에 업로드 실패한 썸네일 바이트를 한 번 더 올려 보는 recoverPendingThumbnailOrNull 을 AfternoteEditorViewModel 에 두었다. UseCase 로 빼지 않은 근거는 #246 의 기준(재사용 1곳 + 약한 로직이면 ViewModel 이 Repository 직접 호출)이다 — 바이트가 ViewModel 소유 상태이고, «실패해도 저장은 간다» 정책은 호출부의 ?: memorialMedia 한 줄이다.
갈림. 이 이슈로 저장 직전 미디어 정리에 슬롯이 하나 더(음성) 생기면, 영상·사진·음성·썸네일 네 갈래 중 썸네일만 ViewModel 에 남는 모양이 된다. 그때 두 선택지가 있다.
선택
내용
대가
(a) 그대로 둔다
썸네일은 실패를 삼키는 갈래라 AfternoteFailure.MediaSave 로 올려 보낼 이유가 없다 — UseCase 에 있을 동기가 애초에 다르다
저장 직전 정리가 두 층에 흩어진 채로 남는다
(b) UseCase 로 모은다
invoke 에 썸네일 바이트 입력을 더해 URL 또는 null 로 해결하되 절대 실패로 종료하지 않는 갈래로 둔다
UseCase 계약과 #349 테스트를 바꾼다. 바이트 소유는 그대로 ViewModel 이라 인자로 넘겨 주는 층이 하나 생긴다
별도 이슈로 만들 만한 크기는 아니라(#1550 대화에서 판단) 여기에만 남긴다. 착수 전 위 표로 결정하고, (b) 로 가면 완료 조건에 한 줄 추가하면 된다.
시안에 없는 기능이라 닫습니다. 추억 노트 음성은 #369 본문의 "(및 음성 녹음)" 한 줄에서 유추한 것이고, 추억 노트 구역 시안에는 사진과 영상만 있습니다. 판정 근거는 PR #1625 의 닫는 코멘트에 있습니다. 서버의 memorialAudioUrl 은 FE 에서 소비하지 않으며, Afternote/Afternote-BE#248 에 같은 내용을 남겼습니다.
Activity
feat(afternote): 추억 노트 사진·영상 즉석 촬영 인텐트 추가
[-]feat(afternote): 추모 미디어 음성 녹음 첨부 — #369 에서 서버 계약 부재로 빠진 갈래[/-][+]feat(afternote): 추억 노트 미디어 음성 녹음 첨부 — #369 에서 서버 계약 부재로 빠진 갈래[/+][-]feat(afternote): 추억 노트 미디어 음성 녹음 첨부 — #369 에서 서버 계약 부재로 빠진 갈래[/-][+]추억 노트 미디어 음성 녹음 첨부 — #369 에서 서버 계약 부재로 빠진 갈래[/+]OnlyReadOneBook commentedon Aug 30, 2026
전제 재판정 — 「서버 계약 부재」는 끝났다
BE
origin/main실코드로 대조했다. 근거는 커밋72fee63feat: 추억 노트 미디어에 음성과 삭제·관리키 검증을 넣는다 (2026-08-30). blocked_by 로 걸어 둔 Afternote-BE#248 도 CLOSED.본문 표와 어긋나는 지점
AfternotePlaylist.memorialAudioUrl(@Column(name = "memorial_audio_url", length = 1000))AfternoteCreateRequest.PlaylistRequest.memorialAudioUrl,AfternoteUpdateRequest.playlist동일AfternotePublishedPlaylistResponse.memorialAudioUrl,ReceivedAfternoteDetailResponse.PlaylistInfo.memorialAudioUrl서버 필드가 선행이라 blocked_byic_mic와 같은 모양으로 신규core:ui에core_ui_ic_mic.xml이 이미 있고 타임레터가 그걸 쓴다 (MediaBottomSheetContent.kt:50)여전히 맞는 것:
MediaKind는PHOTO·VIDEO뿐, presigned 는 mp3·m4a·wav 허용(상한 50MB —max-bytes.audio:52428800),PhotoUploadRepositoryImpl.ALLOWED_EXTENSIONS에 오디오 확장자 포함.계약 세부 — 본문에 없던 제약
afternotes/디렉터리의 관리 객체가 아니면UNMANAGED_MEDIA_URL, 확장자가{mp3, m4a, wav}밖이면INVALID_FILE_EXTENSION— 둘 다 400. 즉 업로드는 반드시DIRECTORY_AFTERNOTES로 가야 하고 확장자가 저 셋 중 하나로 확정돼야 한다.PlaylistRequestDeserializer가 «필드 생략 = 유지» 와 «JSON null = 삭제(DB 참조 + S3 객체)» 를 가른다. 현재 FE 는Json { }기본값(encodeDefaults = false)이라 값이 null 인 필드를 아예 안 실어 보내므로 지금 동작은 «유지» 로 안전하다.착수 전에 바뀌어야 할 항목
1. 녹음 인텐트 —
RECORD_SOUND_ACTION은 확장자를 보장하지 못한다본문의 «
MediaStore.Audio.Media.RECORD_SOUND_ACTION+ 전용ActivityResultContract» 는 계약 관점에서 위험하다. 결과 포맷을 녹음 앱이 정한다 — AOSP 계열은audio/amr·audio/3gpp, 삼성은audio/mp4(m4a). BE 화이트리스트는 mp3·m4a·wav 뿐이라 amr/3gp 가 오면 400 으로 튕긴다. 게다가PhotoUploadRepositoryImpl.resolveExtension은 모르는 MIME 을DEFAULT_EXTENSION = "jpg"로 떨어뜨려서, 녹음 파일이.jpg로 올라간 뒤 오디오 슬롯 검증에서 다시 튕긴다.→ 인앱
MediaRecorder(MPEG_4+AAC→.m4a)가 출력 포맷을 결정적으로 잡는 유일한 길이다. 대신RECORD_AUDIO런타임 권한이 필요해지고(인텐트 경로는 불필요) 매니페스트를 건드리므로 CI Test Plan 이selected/full로 올라간다. 인텐트 경로를 유지한다면 확장자 화이트리스트 밖 결과를 거르는 갈래가 반드시 붙어야 한다.또한 core 에 오디오 전용 업로드 경로가 필요하다 — 지금은
PhotoUploadRepository·VideoUploadRepository뿐이고 전자는 위처럼 오디오 MIME 을 jpg 로 흘린다.2. UI 자리 — 시안 없음 (실측)
Figma 정리 Screen Design 의
애프터노트(4327:99446)·NEW 추억 노트(4327:72816) 두 구역 전체를 REST 로 받아 노드명·텍스트를 «음성·녹음·보이스·오디오·voice·audio·mic» 으로 전수 검색 → 실히트 0건(42건은Dynamic Island spacer오탐). 수신자 열람 쪽도 같다.→ 「미답변 디자인 질의는 FE 재량」 방침대로 시안 부재는 블로커가 아니다. 기존 미디어 첨부 관례(
MemorialPhotoUpload·MemorialVideoUpload슬롯 +MemorialMediaSourceBottomSheet)를 그대로 따라 세 번째 슬롯을 두는 형태로 확정하고 근거를 PR 에 남긴다.3. 재생 UI
수신자 열람
MemorialReceivedDetailScreen에 영상 섹션이 이미 있고,core:common으로 승격된launchMemorialVideo(#1436)가 http/https 스킴만 골라 외부 앱으로 여는 패턴을 쓴다(#1394) — 실패는 스낵바 2문구로 갈린다(#1391). 오디오도 같은 함수를 그대로 태우면 새 플레이어 없이 닫힌다.지금 착수하지 않는 이유 — 파일 경합
계약은 열렸지만 이 기능의 본체(폼 상태 + 저장 배선)가 열린 PR 4건과 같은 파일을 쓴다.
author/editor/state/AfternoteTypeForm.kt,EditorFormMutations.kt,AfternoteEditorViewModel.ktauthor/navigation/**,AppNavigationActions.ktfeature/afternote/presentation/src/main/res/values/strings.xml특히 #1565 가
AfternoteTypeForm.Memorial을 값 객체로 재편하며 picked/server 축을 가르는 중이다. 그 전에 옛 모양 위에 오디오 필드를 얹으면 #1565 가 오디오까지 다시 손봐야 하고, 반대로 #1565 뒤에 붙이면 오디오도 같은 값 객체 규격을 그대로 받는다. 그 아래 계층(MediaKind·ResolveMemorialMediaForSaveUseCase·DTO·매퍼)만 먼저 내면 폼에서 값이 안 흘러 전부 휴면 코드가 된다.→ #1565 머지 후 한 판으로 간다. 순서:
MediaKind.AUDIO+ core 오디오 업로드 → 폼 상태·저장 배선 → 녹음 슬롯 UI + 시트 옵션 → 수신자 재생.갱신된 체크리스트
MediaKind에AUDIO추가 —MemorialMediaUploadRepositoryImpl.resolve의when이 누락을 컴파일 에러로 잡아 준다PhotoUploadRepository는 모르는 오디오 MIME 을jpg로 흘린다ResolveMemorialMediaForSaveUseCase오디오 갈래 +MemorialWritePayload.memorialAudioUrl+ DTO(AfternotePlaylistRequestDto·AfternotePlaylistDto)·매퍼 양방향MediaRecorder(m4a 확정,RECORD_AUDIO권한) vsRECORD_SOUND_ACTION(확장자 비보장, 화이트리스트 필터 필수) 중 택일. 기기 실측으로 확정MemorialMediaSourceState.launchCapture의ActivityNotFoundException갈래 재사용MemorialPhotoUpload·MemorialVideoUpload관례를 따른 세 번째 슬롯 (시안 0건, FE 재량)MemorialMediaTarget.AUDIO(아이콘은core_ui_ic_mic재사용, 신규 제작 불필요)core:common의launchMemorialVideo(refactor(core): 추모 영상 실행 가드가 발신자·수신자 2벌 — 37줄 문자 단위 동일본을 core:common 으로 승격 #1436) 를 그대로 태워 스킴 가드 + 스낵바 2문구 재사용곁가지 — 같은 커밋이 #1406 의 전제도 바꿨다
MemorialMediaSourceBottomSheet.removableMemorialMediaTargets의 KDoc 은 서버에 저장된 미디어를 삭제 대상에서 뺀 근거를 이렇게 적고 있다:그 «BE 계약 확장» 이 같은 커밋
72fee63으로 들어왔다 — 이제 JSON null 을 명시하면 DB·S3 에서 지워진다. 근거가 사라진 주석이고, 그 축은 #1406 이 잡고 있다.OnlyReadOneBook commentedon Aug 30, 2026
본문의 「서버 자리 없음」이 낡았습니다 — BE 계약이 오늘 열렸습니다 (2026-08-30)
이 이슈는 본문에 「서버 업로드는 mp3·m4a·wav 를 허용하는데 추억 노트에 붙여 둘 필드가 없어 저장 시점에 버려진다」고 적고, 그래서 서버 선행을 blocked_by 로 걸어 뒀습니다. 그 전제가 오늘 해소됐습니다.
실측:
feat: 추억 노트 미디어 음성·삭제·관리키 검증memorialAudioUrl즉 「올릴 수는 있는데 붙여 둘 필드가 없다」는 더 이상 사실이 아닙니다. 본문 표의 「저장 페이로드 (
memorialPhotoUrl·memorialVideo) — 자리 없음」 행이 낡았습니다.그래도 지금 당장 착수하지는 않았습니다 — 파일이 겹칩니다
FE 쪽
MediaKind는 여전히PHOTO·VIDEO뿐이라 할 일 자체는 본문 그대로입니다. 다만 이 이슈가 건드릴 파일(추억 노트 미디어 폼·DTO·미디어 선택 시트)을 지금 열려 있는 PR 들이 동시에 만지고 있습니다:picked/server축)core:ui로 승격특히 #1605 가 승격한
MediaSelectSheetContent는 항목 개수를 계약에 박지 않았습니다(items: List<MediaSheetItem>). 그 스택이 착륙한 뒤에 시작하면 음성 항목은 호출부에서 원소 하나 더 넣는 것으로 끝나고core:ui는 안 건드려도 됩니다.정리 — 이 이슈의 남은 선행 조건은 「서버 계약」이 아니라 「위 스택 착륙」입니다. blocked_by 를 손대야 하면 그 기준으로 다시 거는 게 맞습니다.
착수할 때 확인할 것
RECORD_SOUND_ACTION의 전용ActivityResultContract, 녹음 앱 부재 처리, 재생 UI 판단)은 그대로 유효합니다.OnlyReadOneBook commentedon Aug 30, 2026
본문의 전제가 낡았다 — 서버 자리는 이미 생겼다 (2026-08-31 재판정)
본문은 「저장 페이로드에 음성을 실을 자리가 없다」를 전제로 Afternote-BE#248 을 blocked_by 로 걸어 뒀는데, 그 블로커는 2026-08-30 에 닫혔다. BE
origin/main8e605f4실코드로 확인했다 (OpenAPI 는 전 필드를 optional 로 보여 주므로 근거로 쓰지 않았다):AfternoteCreateRequest.PlaylistRequest#L115String memorialAudioUrl— nullable, 추억 노트당 1개AfternotePlaylist#L35memorial_audio_url VARCHAR(1000)S3Service#L83mp3·m4a·wavPlaylistValidationStrategy#L84#L77memorialAudioUrlnullable구현 커밋은
72fee63(PR Afternote/Afternote-BE#268).본문에 함께 적힌 「PATCH 가 첨부 삭제를 표현하지 못한다」도 같은 커밋에서 풀렸다.
PlaylistRequestDeserializer가 「키 없음」과 「키 있고 JSON null」을 갈라AfternotePlaylist.update의 specified 플래그로 넘긴다 — 즉 명시적 null 을 보내면 DB 참조와 S3 객체가 함께 지워진다.구현 — PR #1625
위 판정에 따라 체크리스트를 이렇게 처리했다.
MediaKind.AUDIO·ResolveMemorialMediaForSaveUseCase오디오 갈래 · 저장 페이로드 매핑 — 완료.RECORD_SOUND_ACTION은 결과 URI 를 인텐트가 돌려주는 계약이라 전용ActivityResultContract(RecordSoundContract)를 새로 짰다.ActivityNotFoundException갈래를 그대로 재사용.PickVisualMedia) 대상이 아니라 문서 선택기(SAF)로 골라야 해서 첫 항목의 아이콘·문구도 슬롯을 따라 갈린다. 아이콘은 신규로 그리지 않고core_ui_ic_mic(타임레터 시안과 같은 모양)을 재사용했다.4327:72816)에추억노트_사진추가·추억노트_영상 추가만 있고 음성 프레임이 없다. 파일 전체에서 「음성 추가하기」가 나오는 곳은 타임레터 블록 시트뿐이다. 그래서 영상 슬롯 바로 뒤에 같은 규격으로 「남기고 싶은 목소리」 섹션을 새로 뒀다 — 근거는 PR 본문에.아직 안 한 것
이 이슈는 열어 둔다 — PR #1625 는
Closes가 아니라Refs로 걸었다. 아래 항목은 본문 체크리스트의 마지막 줄(재생 UI)이 계속 추적하며, 담당은 그대로 나(afternote·receiver 모듈)다. 남에게 넘기는 몫은 없다.참고로 첨부 단계에서 막은 형식이 있다 — 안드로이드 기본 녹음기가 흔히 내놓는
audio/amr·audio/3gpp는 서버가 안 받아 첨부 시점에 안내하고 되돌린다. 서버가 형식을 늘리면MemorialAudioFormats한 곳만 고치면 된다.OnlyReadOneBook commentedon Sep 2, 2026
착수 시 함께 판단할 것 — 썸네일 저장 직전 재업로드를
ResolveMemorialMediaForSaveUseCase로 모을지 (2026-09-02)완료 조건의 「
ResolveMemorialMediaForSaveUseCase에 오디오 갈래 추가」 를 할 때, 같은 자리에 썸네일 갈래도 모을지 한 번에 결정한다. 두 번 손대지 않기 위한 메모다.현황. #1550 (PR #1567) 이 추모 영상 썸네일 실패에 재시도를 붙이면서, 저장 직전에 업로드 실패한 썸네일 바이트를 한 번 더 올려 보는
recoverPendingThumbnailOrNull을AfternoteEditorViewModel에 두었다. UseCase 로 빼지 않은 근거는 #246 의 기준(재사용 1곳 + 약한 로직이면 ViewModel 이 Repository 직접 호출)이다 — 바이트가 ViewModel 소유 상태이고, «실패해도 저장은 간다» 정책은 호출부의?: memorialMedia한 줄이다.갈림. 이 이슈로 저장 직전 미디어 정리에 슬롯이 하나 더(음성) 생기면, 영상·사진·음성·썸네일 네 갈래 중 썸네일만 ViewModel 에 남는 모양이 된다. 그때 두 선택지가 있다.
AfternoteFailure.MediaSave로 올려 보낼 이유가 없다 — UseCase 에 있을 동기가 애초에 다르다invoke에 썸네일 바이트 입력을 더해 URL 또는 null 로 해결하되 절대 실패로 종료하지 않는 갈래로 둔다별도 이슈로 만들 만한 크기는 아니라(#1550 대화에서 판단) 여기에만 남긴다. 착수 전 위 표로 결정하고, (b) 로 가면 완료 조건에 한 줄 추가하면 된다.
OnlyReadOneBook commentedon Sep 2, 2026
첨부·녹음 경로(해야 할 일 1~6)를 #1734 로 분리했고 PR #1625 가 그 이슈를 Closes 합니다.
이 이슈에는 7번 — 붙인 음성을 다시 듣는 수단(작성자 상세·수신자 열람, 영상 재생과 함께) — 만 남습니다. 블로커 없음(BE#248 닫힘).
OnlyReadOneBook commentedon Sep 11, 2026
시안에 없는 기능이라 닫습니다. 추억 노트 음성은 #369 본문의 "(및 음성 녹음)" 한 줄에서 유추한 것이고, 추억 노트 구역 시안에는 사진과 영상만 있습니다. 판정 근거는 PR #1625 의 닫는 코멘트에 있습니다. 서버의
memorialAudioUrl은 FE 에서 소비하지 않으며, Afternote/Afternote-BE#248 에 같은 내용을 남겼습니다.