← Projects
프로젝트 상태: 완료 · 서버 종료 🏆 2025 관광데이터 활용 공모전 우수상 (한국관광공사, 2025.11)

HeatTrip

감정 입력(감정 칩, PAD 슬라이더, 에너지, 사교성, 여행 목표)으로 한국관광공사 장소 데이터에서 여행지를 추천하고 일정·다이어리로 잇는 모바일 여행 서비스입니다. 2025 관광데이터 활용 공모전 3인 팀의 팀 리드로 기획, 서버의 장소 수집·탐색·추천·미디어·인증, Python 추천 서버, 앱의 추천·탐색 화면, 배포와 원스토어 출시를 맡았습니다. 2026년 3–6월에는 서버를 혼자 다시 정비했습니다. 서버는 2026-10 현재 운영하지 않습니다.

Role

3인 팀 리드 — 기획, 백엔드(장소 수집·탐색·추천·미디어·인증), Python 추천 서버, 앱 추천·탐색 화면, 배포·원스토어 출시

Period

2025.07–11 공모전(팀 Hit다Heat, 3인) · 2026.03–06 개인 리팩토링

Java 21 · Spring Boot 3.5 MySQL 8 (FULLTEXT ngram) Python · FastAPI · Pydantic OpenAI API (추천 서버) Flutter AWS S3 · CloudFront Spring Security · JWT JPA Docker Compose Kotlin (2026)
HeatTrip 앱 화면 5종 — Explore, 감정 입력(PAD 슬라이더), For You 분석 결과, 추천 여행지, Diary
공모전 기간 최종 배포본 — 원스토어 1.0.0(3), 2025-10

약 5만 곳

수집한 관광공사 장소

로컬 DB 50,098건 기준(2026-10-01)

95개

LLM이 고르는 카테고리 목록

닫힌 목록 · 장소 목록은 프롬프트에 넣지 않음

일정 API·다이어리 v1은 팀원 A가, 북마크·프로필은 팀원 B가 만들었습니다. 앱은 원스토어에 1.0.0을 2025-09-15에 등록했고, 1.0.0(3)을 2025-10-23에 다시 올렸습니다.

팀과 맡은 범위

2025 팀 시기 기준입니다. 서버는 패키지, 앱은 기능 단위로 적었습니다.

영역본인팀원 A팀원 B
기획팀 리드(기획·총괄)풀스택 개발풀스택 개발
서버장소 수집·보강(tour, kakao), 탐색·검색(explore), 추천(curation), 미디어(S3), 인증(OAuth, user 대부분), 다이어리 v2 API, 배포 설정일정 CRUD, 다이어리(journey) 엔티티·v1 API북마크·컬렉션, 회원 정보 수정·탈퇴, 의견 보내기
추천 서버Python(FastAPI) 전부——
앱For You v1·v2, Explore 대부분, 인증·기록·미디어, 배포 빌드다이어리 일부북마크·프로필, 다이어리 화면 일부, Explore 일부, 디자인 수정
배포·출시Dockerfile·compose(2025-09-12), 원스토어 등록(판매자 본인)——

시스템 구성

HeatTrip 시스템 구성도. Flutter 앱이 Spring 서버에 REST로 붙고, 장소 상세는 앱이 관광공사 API를 직접 부릅니다. Spring은 MySQL, S3·CloudFront, 관광공사 수집 API, Kakao Local을 쓰고, 추천은 Python 추천 서버를 거쳐 OpenAI에 묻습니다. 금색은 본인, 회색은 팀원 A·B, 청록은 2026 개인 추가이고, OAuth2 공급자는 서버에만 구현돼 점선으로 그렸습니다.
2025 팀 구성과 2026 개인 추가(청록) — 백엔드 47d161f·034c3dc, 앱 5313bc7

원본 크기로 보기 ↗

  • 장소 데이터는 서버가 모으고, 장소 상세의 ‘개요’는 앱이 관광공사 API를 직접 부릅니다.
  • 추천 LLM은 별도 Python 서버에 있고, Spring은 그 결과를 장소 순위로 바꿉니다. 대가는 요청 경로에 동기 호출이 한 단계 늘고, 그동안 톰캣 스레드가 기다린다는 점입니다.

추천: LLM이 고르는 것과 Spring이 하는 것

추천 한 번의 시퀀스(v2). 앱이 감정 값과 목표 키워드를 Spring에 보내면, Spring은 장소 목록 없이 감정 값과 키워드만 Python 추천 서버로 넘깁니다. 추천 서버는 95개 카테고리의 닫힌 목록으로 OpenAI에 묻고, 검증한 카테고리 라벨을 돌려줍니다. Spring은 라벨을 cat3 코드로 바꿔 MySQL에서 후보를 읽고 점수식으로 정렬해 앱에 돌려줍니다. 그림의 메모: pad·energy는 서버 DTO가 정수라 잘린 값이 단계 2와 10에 쓰입니다.
추천 한 번(v2, 스토어 1.0.0(3)) — 코드 줄은 034c3dc

원본 크기로 보기 ↗

장소 목록은 프롬프트에 넣지 않습니다. LLM은 95개 카테고리 이름 중에서 골라 두 그룹으로 묶어 돌려줍니다. 출력이 닫힌 목록이라, Spring은 메모리에 올린 카테고리 사전(place_traits 95행)으로 라벨을 cat3 코드로 바꿔 장소를 거릅니다. 대가로 사전에 없는 라벨은 버려지고, 매핑이 비면 장소 전체를 읽습니다(findAll()).

순위는 LLM이 아니라 Spring의 점수식이 정합니다. 최종 점수는 0.6×특성 일치 + 0.4×인기도입니다. 앱이 위치를 보내면 거리 점수를 0.2 비중으로 섞습니다. 상수는 손으로 정했고, 데이터로 검증한 기록은 없습니다.

추천 서버가 LLM 출력의 형식을 검증하고 고칩니다. 응답은 JSON 모드로 받아 Pydantic으로 검증하고, 실패하면 재요청, 위로 편지 보정, 정규화 순으로 처리합니다. 같은 입력은 5분 동안 캐시합니다. 캐시는 프로세스 메모리에 있어 인스턴스 하나를 전제로 합니다.

공모전 제출본(2025-09-19 제출)은 LLM이 없는 v1(2025-09-10, b6fac1c)입니다. v1은 전체 장소에 점수를 매기고, 감정 카테고리 18개(매핑 72개)로 묶어 보여 줬습니다. LLM을 쓰는 v2는 2025-10-18(58da1b9)에 들어와 스토어 1.0.0(3)에 실렸습니다. 두 판은 같은 ScoringService를 씁니다.

장소 데이터: 수집과 모델

장소 데이터의 수집 순서와 개요 ERD. 수집은 관광공사 지역 목록 → 정제·절단 → places 배치 저장 순서이고, 그 뒤 Kakao 링크·좌표 보강과 CAT3 특성 스냅숏 → search_text(2026)로 갈라집니다. 아래에는 장소 카탈로그(places, place_traits, place_trait_snapshots, place_features, place_kakao_links), 감정 분류, 감정 피드백, 사용자·콘텐츠 묶음의 주요 표와, FK 실선과 FK 없는 논리 키 점선이 있습니다.
장소 데이터의 수집 순서와 주요 표 — 규모는 2026-10-01 로컬 DB

원본 크기로 보기 ↗

  • 수집. 관광공사 지역 목록(areaBasedList2)을 받아 정제·절단(Place.sanitizeAndTruncate)한 뒤 배치로 저장합니다. 저장 뒤에는 두 작업이 갈라집니다. 하나는 Kakao Local로 링크와 좌표를 보강하는 작업이고(속성 플래그로 한 번 실행), 다른 하나는 CAT3 특성 스냅숏을 만들어 2026년부터 search_text를 채우는 작업입니다. 스케줄러는 주석 처리돼 있어 수동으로 돌리고, 실행 이력은 남기지 않습니다.
  • 규모. 장소는 50,098곳(이미지 있는 곳 38,871)입니다. 카테고리 사전은 95행, 감정 카테고리는 18개(매핑 72개)입니다.
  • 키. 장소 쪽 표는 FK 없이 contentid와 cat3로 잇습니다. 스키마는 Hibernate ddl-auto로 관리하고(마이그레이션 도구 없음), FULLTEXT 인덱스는 수동 SQL로 만듭니다.
  • 무한 스크롤. (createdtime, contentid) 커서를 Base64URL 토큰으로 주고받고, size+1개를 읽어 다음 페이지가 있는지 판단합니다. contentid를 둘째 키로 둬 같은 시각 행의 중복·누락을 막습니다. 목록 화면은 오프셋을 그대로 씁니다. 대가로 커서를 받칠 복합 인덱스 DDL이 레포에 없습니다.
  • 미디어. DB에는 S3 키만 저장하고, URL은 CloudFront 도메인과 키로 실행 때 만듭니다. 어디에 붙는 파일인지는 ref_type/ref_id로 적고(FK 아님), 삭제는 소프트 삭제입니다.

2026 개인 리팩토링

2026-03-19부터 06-18까지 혼자, 공개 저장소와 운영을 기준으로 서버를 다시 정비했습니다.

영역바꾼 것(커밋)대가
공개 운영 정비/api/curation/**에 인증 요구, 인증 없이 열어 둔 경로 축소, 경로별 레이트 리밋(메모리 고정 윈도), gitleaks 비밀 스캔(934caca)이미 배포된 1.0.0(3)은 토큰을 보내지 않아 For You가 302로 실패하게 됨. 하위 호환은 확인하지 않음
검색EXPLAIN ANALYZE로 count 쿼리의 전체 스캔과 상관 서브쿼리 반복을 확인. 검색할 문자열을 search_text 컬럼 하나로 반정규화하고 FULLTEXT(ngram) 인덱스와 MATCH … AGAINST … IN BOOLEAN MODE로 전환(0a72056)결과가 LIKE와 다름(한 글자, 특수문자). 정렬은 관련도가 아니라 contentid 순
추천 호출 분리·테스트·관측성추천 서버 호출을 Kotlin 포트/어댑터로 옮기고(12d351c, 2026-04-19) Kotlin DTO와 단위 테스트 추가. 요청 로그·에러 알림 AOP, correlation id, 마스킹. 에러는 fingerprint로 중복을 억제해 Slack으로 보내고 Spring AI 요약을 붙일 수 있음검색·LLM DTO 테스트 일부는 변경을 되돌려도 통과. 관측성 코드는 테스트와 실제 발송 기록이 없음

검색의 변경 전 count는 약 755ms, 변경 후 목록은 약 4.9ms(‘카페’ 1회 측정)이고, 두 수치는 서로 다른 쿼리의 것입니다. 검색은 2025-09 팀 시기에 포트/어댑터로 떼어 두었기 때문에(df3044f) 저장소 쪽만 바꿔 전환했습니다.

앱도 하단 탭을 오늘·탐색·[기록]·피드·나로 바꾸고 감정 체크인과 피드를 더했지만 배포하지 않았습니다. 아래는 2026-10-01에 로컬 백엔드와 데모 데이터로 실행해 캡처한 화면입니다.

한계(고친다면)

한계영향고친다면
감정 값(PAD·에너지)을 서버 DTO가 정수로 받습니다소수 입력이 대부분 0이 되어, 순위와 LLM 입력에 감정 특성이 거의 반영되지 않습니다. LLM에 온전히 가는 입력은 감정 칩, 목표 키워드, 사교성뿐입니다실수 타입으로 바꾸고 앱–서버 계약 테스트를 둡니다
추천 서버 호출: 180초 타임아웃이 재시도마다 걸리고 4xx도 재시도하며, 톰캣 스레드가 .block()으로 기다리고 대체 응답이 없습니다추천 서버나 OpenAI가 느리면 요청 스레드가 오래 묶이고 추천이 실패합니다짧은 타임아웃, 4xx 재시도 제외, 규칙 기반(v1) 대체 응답을 둡니다
매핑이 비면 장소 전체를 읽고(findAll()), 커서를 받칠 복합 인덱스 DDL이 없으며, 스키마는 ddl-auto입니다큰 표를 통째로 읽거나 정렬하는 쿼리가 생깁니다빈 매핑은 빈 결과로 돌려주고, 인덱스와 스키마는 마이그레이션으로 관리합니다
테스트가 회귀를 잡지 못합니다(되돌려도 통과). 관측성 코드는 테스트·발송 기록이 없습니다바꾼 뒤 안전한지 테스트로 알 수 없습니다되돌리면 실패하는 테스트부터 씁니다
공개 앱 main이 컴파일되지 않습니다(스토어 빌드는 5313bc7, 복구 PR #9 열림)링크한 main으로는 출시 앱을 다시 빌드할 수 없습니다PR #9를 병합합니다

백엔드와 앱 코드는 공개되어 있고, Python 추천 서버는 비공개입니다. 서버는 2026-10 현재 운영하지 않고, 원스토어 등록 기록만 남아 있습니다. 2026-09-30 이후 레포의 정리 커밋(README, 설정 클래스 등)은 AI 코딩 에이전트가 작성했고 트레일러로 표기했습니다. 2026-06의 앱 재설계 시도는 공개 레포에 없고, 이후 Pawloop으로 이어졌습니다.