🐝매일 한입
개발일지📖 12분 읽기

Joiny — 저장해 둔 일정 이미지로 새 여행을 만드는 가져오기, 확인 화면을 거쳐서만

다른 앱에서 저장한 일정 이미지를 읽어 새 여행을 채우는 기능을 하루에 만들었습니다. 긴 이미지는 겹치게 잘라 보내고, 장소는 나라를 확인해 붙이고, 여행은 확인 화면을 거친 뒤 서버에 있는 결과로만 만듭니다.

📖 12분 읽기
#Joiny#일정 가져오기#Vision 모델#Supabase Edge Functions#Google Places#1인개발

사진첩에 남는 건 일정 이미지 한 장

트리플이 내일(11월 3일) 서비스를 끝내고 NOL 로 통합됩니다. intent 에 옮겨 적은 공지로는 예약 정보·일행·가계부·체크리스트는 옮겨지지 않고, 공유 링크도 닫힙니다. 끝까지 남는 것은 공식 백업인 "일정 이미지로 저장하기"로 만든 이미지이고, 그건 사람들의 사진첩에 그대로 있습니다.

그런데 Joiny 에는 다른 곳에서 짠 일정을 들여올 길이 없었습니다. 장소 15~20개를 한 줄씩 검색해 다시 넣어야 하니, 갈아타려던 사람도 빈 여행 앞에서 멈춘다고 intent 에 적었습니다. 수요는 종료 직후 몇 주가 가장 큽니다.

그래서 오늘 하루에 intent·spec·plan 을 쓰고 구현까지 했습니다. 홈의 첫 사용 화면이나 "새 여행" 화면에서 "일정 이미지로 가져오기"를 누르고 이미지를 최대 10장 고르면, 20초쯤 뒤 읽은 결과로 채워진 확인 화면이 열립니다. 거기서 "여행 만들기"를 눌러야 비로소 여행이 생깁니다. 무료이고 계정당 월 5회이며, 실패한 가져오기는 세지 않습니다. 트리플은 화면에 예시로만 쓰고 "트리플은 NOL Universe의 상표이며 Joiny와 관계가 없어요"라는 고지를 답니다.

긴 이미지를 자르는 법, 그리고 모델 고르기

트리플의 일정 이미지는 세로로 아주 깁니다. 제가 모은 15일짜리 샘플은 1179×32,924 픽셀이었습니다. 이걸 한 장으로 모델에 보내면 글자가 뭉개지니, 너비를 1024 로 줄인 뒤 높이 1536, 겹침 256 으로 잘라 보냅니다. 앱·Edge Function·평가 스크립트가 같은 파일을 읽도록 import 없는 순수 TS 로 뺐습니다.

/**
 * 너비를 맞춘 이미지의 세로 조각. 조각은 STRIP_OVERLAP만큼 겹치고, 마지막 조각은 아래 끝에 맞춘다.
 * 겹침(256px ≈ 원본 1179px 너비에서 295px)이 트리플 카드 한 장(약 215~280px)보다 커서, 모든 카드가 적어도 한 조각에 통째로 들어간다.
 */
export function stripsFor(height: number, size = STRIP_HEIGHT, overlap = STRIP_OVERLAP): Strip[] {
  if (height <= size) return [{ y: 0, height }];
  const step = size - overlap;
  const out: Strip[] = [];
  for (let y = 0; y + size < height; y += step) out.push({ y, height: size });
  out.push({ y: height - size, height: size });
  return out;
}

겹침을 카드 한 장보다 크게 잡아서, 경계에서 잘린 카드도 어느 한 조각에는 통째로 들어갑니다. 대신 같은 카드가 두 번 보일 수 있어 서버가 같은 Day 에서 바로 이어 나온 같은 제목을 하나로 합칩니다. 상한은 40조각(약 25일치)이고, 넘으면 기기에서 보내기 전에 막습니다.

모델은 새 위탁사를 늘리지 않으려고 영수증 인식과 같은 OpenAI 키를 씁니다(ADR-0011 에 한 줄 더함). 고르는 일은 평가 스크립트로 했습니다. 트리플 내보내기 샘플 다섯 종(2~15 Day)과 대전을 Day별 두 장으로 나눈 것을 넣고, 사람이 만든 정답 JSON 과 비교했습니다.

spec 의 결론은 gpt-5.6-luna 입니다. 12회 실행에서 빠진 항목·다른 Day 로 간 항목·틀린 시각이 모두 0이었고, 9조각 이하는 30초 안, 1회 $0.003~0.006 이었습니다. 더 싼 gpt-6-luna 는 제목이 더 정확했지만 추론 토큰이 들쭉날쭉해 15일 일정에 51.6초가 걸렸고, effort 를 low 로 낮추면 항목 두 개를 빠뜨렸습니다. 일정을 옮기는 기능에서 항목이 빠지는 건 제목 한 글자보다 나쁘다고 봤습니다. gpt-6.1-terra 는 정확도가 같은데 값이 5배라 뺐습니다. 남은 오류는 "유니버셜"을 "유니버설"로 고쳐 버리는 식의 한두 글자라, 확인 화면에 보이게 두고 만든 뒤 고치게 했습니다.

장소를 붙일 때는 나라를 본다

지도 핀은 AI 일정 초안의 attachPlaces 를 공용으로 옮겨 붙입니다. 가져오기에서 바꾼 것은 둘입니다. 검색어에 도시 이름을 붙이지 않습니다("돈카츠 와카바 치쿠시구치 점"에 "후쿠오카"를 붙이자 텐진점이 잡혔습니다). 그리고 결과의 나라를 봅니다. 도시 쪽으로 기울여 검색해도 권고일 뿐이라 후쿠오카 일정의 "공유 오피스"가 부산에서 잡혔기 때문입니다. 여행 도시들의 나라가 아니면 장소 없이 넣습니다.

이 확인이 시뮬레이터에서 거꾸로 작동했습니다. 같은 15일 일정을 두 번째로 가져오자 핀이 39곳에서 16·18곳으로 줄었습니다. 좌표가 캐시에 있어 나라를 새로 받지 않았고, 캐시 좌표는 도시에서 300km 안인지로만 보니 마드리드 여행의 바르셀로나 장소가 빠졌습니다. 고친 판정은 이렇습니다.

  const inTrip = (id: string, pin: PlacePin): boolean => {
    if (!checkCountry) return true;
    const country = countryById.get(id);
    if (country) return allowed.has(country);
    if (!foundIdSet.has(id)) return true;
    const around = cities && cities.length > 0 ? cities : [city];
    return around.some((c) => distanceKm(c, pin) <= FAR_KM);
  };

이름 캐시에서 꺼낸 장소는 전에 나라를 확인하고 남긴 것이라 믿고, 이번에 새로 찾은 장소는 좌표가 캐시에 있어도 나라를 다시 받습니다. 다시 해서 39곳으로 돌아왔다고 plan 에 적혀 있습니다. 장소 연결은 로컬 확인에서 대전 17/17, 후쿠오카 19/20, 삿포로 34/34 로 intent 의 기준(80%)을 넘었습니다.

확인 화면을 새로 만든 이유, 확인한 것과 남은 것

AI 일정 초안의 미리보기 화면을 "가져오기 모드"로 다시 쓰는 길도 있었지만, spec 은 새 화면을 택했습니다. 그 화면은 이미 있는 여행에 묶여 있고 대부분이 수정 대화·페이월·되돌리기라 가져오기에는 필요 없으며, 반대로 제목·날짜·도시 편집과 장소 연결 풀기는 없습니다. 모드로 묶으면 분기마다 두 흐름을 다 시험해야 한다는 것이 기록된 이유입니다.

읽은 일정 확인 화면: 여행 이름 '오사카 2박 3일', 날짜 12.18~12.20, 도시 칩 오사카, 번호 핀이 찍힌 지도, Day 1 의 시각별 항목과 닫기·여행 만들기 버튼 1.2.0 스토어 스크린샷 2번. 가상의 오사카 일정 결과를 로컬 DB 에 넣고 연 확인 화면으로, 여행 정보·지도·Day 목록이 이 절에서 말한 순서대로 놓여 있습니다.

여행을 만드는 RPC create_trip_from_import 는 앱이 보낸 제목·메모·장소를 받지 않습니다. 앱은 남길 항목의 key 와 장소 연결 풀기 여부만 보내고, 내용은 서버에 저장된 결과에서 꺼냅니다. 이미지는 저장하지 않고, 읽은 결과는 30일 뒤 지웁니다.

plan 에 남은 검증은 pgTAP 43개(월 5회 한도, 남의 결과 막기, 실패하면 여행이 생기지 않는 한 트랜잭션 등), 로컬 함수와 실제 모델로 돌린 샘플 6건(Day·날짜 모두 맞음, 15일 일정 40.5초), 그리고 iPhone 17 Pro 시뮬레이터 확인입니다. 시뮬레이터에서 앞의 핀 문제를 포함해 4건을 고쳤습니다. 그 스크린숏은 제 실제 여행 일정이 보여서 레포에 넣지 않았습니다.

남은 것도 적어 둡니다. 원래 공개일은 개정 처리방침이 시행되는 11월 10일이었는데, 오늘 당일 공개로 앞당기고 처리방침 해당 항목의 시행일과 안내 시트 문구("고른 이미지는 OpenAI(미국)로 보내 일정을 읽고, 저장하지 않아요")를 바꿨습니다. 하지만 plan 의 "10. 배포" 체크박스는 아직 비어 있고, Places 할당량 상향(500 → 2,000)과 운영 빌드 인수 확인도 결과가 레포에 없습니다. 메모 정확도는 다시 돌린 평가에서 35/39 로 기준에 하나 모자랐고, 트리플 밖 다른 앱의 이미지로는 아직 평가하지 않았습니다.

Joiny 는 함께 갈 사람을 초대해 같이 여행 일정을 짜고, 지도로 동선을 보고, 비용을 정산하는 여행 플래너입니다. 지금 iOS 에서 무료로 쓸 수 있습니다.

📚 관련 글

💬 댓글