준비 항목에 사진을 붙이다
재료, 도구, 장소에 사진을 첨부할 수 있게 했습니다. 기존 첨부파일 패턴을 재사용하면서, 준비 항목이라는 새로운 맥락에 맞게 조정한 과정.
준비 항목에 사진을 붙이다
글로 적는 것만으로 부족할 때
준비 섹션에는 재료, 도구, 장소, 인력, 자격 다섯 카테고리가 있습니다. 이름과 설명, 수량, 링크를 적을 수 있었습니다. 하지만 “이 재료가 어떻게 생겼는지”, “이 장소가 어디인지”를 글로만 설명하기엔 한계가 있었습니다.
사진 한 장이면 충분한 것을, 문장으로 풀어 적고 있었습니다.
어디에 붙일 것인가
태스크 자체에는 이미 첨부 사진 기능이 있었습니다. StoredFile로 파일을 저장하고, 썸네일을 생성하고, 다운로드하는 흐름이 갖춰져 있었습니다. 문제는 이걸 준비 항목에 어떻게 연결하느냐였습니다.
태스크의 첨부파일은 attachments: [{order, stored_file_id}] 배열로 관리됩니다. 준비 항목에도 같은 구조를 넣었습니다. 모든 카테고리의 베이스 모델에 attachments 필드를 추가하고, 현재는 1장으로 제한했습니다. 배열 구조이므로 나중에 여러 장으로 확장할 수 있습니다.
다섯 중 셋만
재료, 도구, 장소 — 이 셋에만 사진을 넣었습니다. 인력과 자격은 사진이 필요한 맥락이 아니었습니다. 사람의 사진을 붙이는 건 어색하고, 자격증 사진은 별도의 문서 관리 영역입니다.
서버에서 일어나는 일
업로드 엔드포인트를 만들었습니다. POST /{task_id}/{category}/{order}/image/upload. 기존 첨부파일 패턴을 따랐습니다.
- 이미 사진이 있으면 거부 — 삭제 후 다시 올려야 합니다
- 파일을 저장하고 512px 썸네일을 생성합니다
StoredFile도큐먼트를 만들고, 준비 항목의attachments배열에 추가합니다
삭제는 DELETE /{task_id}/{category}/{order}/attachment/{attachment_order}/delete. 배열에서 해당 항목을 제거합니다.
태스크 레코드와 태스크 템플릿 양쪽에 동일한 엔드포인트를 등록했습니다.
앱에서 보이는 것
PreparationItemPhotoInput이라는 공통 컴포넌트를 만들었습니다. 재료, 도구, 장소 편집 화면 세 곳에서 동일하게 사용합니다.
사진이 없으면 추가 버튼이 보입니다. 카메라로 찍거나 갤러리에서 고를 수 있습니다. 사진이 있으면 썸네일이 보이고, 탭하면 원본을 확대해서 볼 수 있습니다. 확대 화면에서 삭제도 할 수 있습니다.
데스크톱에도 같은 기능을 넣었습니다. 파일 선택 → 업로드 → 썸네일 표시 → 호버 시 삭제 버튼.
직렬화를 빠뜨리다
처음 구현했을 때, 사진을 올리면 화면을 나갔다 들어와야 보이는 버그가 있었습니다. 원인은 TaskModel.toJSON()에서 준비 항목의 attachments를 빠뜨린 것이었습니다.
앱은 SQLite 캐시를 사용합니다. 태스크를 JSON으로 직렬화해서 저장하고, 다시 읽을 때 역직렬화합니다. toJSON()에 attachments가 없으니, 저장할 때 사라지고 다시 읽으면 없는 것처럼 보였습니다.
한 줄씩 추가하는 것으로 해결했습니다. 하지만 이런 종류의 버그는 필드를 추가할 때마다 반복될 수 있습니다. 직렬화를 수동으로 관리하는 구조의 약점입니다.
사진 한 장의 무게
기능적으로는 작은 변경입니다. 하지만 “밀가루 500g”이라고 적힌 항목 옆에 실제 밀가루 봉지 사진이 있으면, 준비 섹션의 실용성이 달라집니다. 글자만으로는 전달하기 어려운 맥락을 사진이 채워줍니다.