오름 (OREUM)
등산 경로를 지도 위에 구간 단위로 기록하고 나누는 등산 커뮤니티 웹 서비스입니다. KDT 풀스택 캠프(강남 하이미디어) 졸업 프로젝트로 4인 팀이 만들었고 졸업 프로젝트 우수상을 받았습니다. 백엔드·프론트 두 레포의 초기 구조와 서버 설정, 지도(산 검색·산악 예보·등산로), 큐레이션 글, 소셜 로그인·S3 업로드를 서버와 화면 양쪽에서 맡았습니다. 시연은 로컬 개발 서버에서 했고 소스는 비공개입니다.
Role
4인 팀 — 레포 초기 구조·서버 설정, 지도·산악 예보, 큐레이션 글, 소셜 로그인·S3 업로드 (Spring Boot·React)
Period
2025.06 – 2025.07 (KDT 졸업 프로젝트)
팀은 2025년 6월 2일부터 7월 3일까지 만들었고, 그중 맡은 기능 줄기는 산 지점 → 산악 예보 캐시 → 지도 → 큐레이션 글(경로를 구간 단위로 기록하는 글) → 지도 위 작성자입니다. 굿즈·결제, 일반 로그인·가입·마이페이지, 홈 피드, 서브 커뮤니티 생성과 피드는 팀원이 맡았고, 레포에는 배포 설정이 없습니다.
시연
큐레이션 글. 지도에 마커를 찍어 경로를 그리고, 구간에 지점명·편의시설·설명과 영상을 넣은 뒤, 상세 화면에서 구간 카드와 경로 지도를 봅니다.
지도. 산을 검색해 날짜·시간대별 예보 카드와 등산로를 보고, 등산로 정보 팝업을 연 뒤 작성자 프로필 마커를 눌러 큐레이션 글로 갑니다.
두 클립은 팀 발표 자료(Figma)의 시연 영상(2025년 7월)에서 기능 구간만 잘라 소리 없이 넣었습니다. 로컬 개발 서버 화면이고, 화면 속 닉네임은 서비스가 자동으로 만든 값입니다.
맡은 범위
| 영역 | 본인 | 팀원 |
|---|---|---|
| 레포 골격·서버 설정 | 두 레포의 초기 구조(Spring Boot 프로젝트, React 라우트·Redux·개발 프록시), 서버 설정·빌드 파일 관리 | CORS 허용 목록 일부 |
| 지도 | 산 검색, 산악 예보 수집과 Redis 캐시, 등산로 표시, 지도 위 작성자 프로필 — 서버와 화면 | — |
| 큐레이션 글 | 작성 화면(지도 위 마커·경로, 구간 입력), MySQL 글과 MongoDB 구간 저장, 목록·검색, 상세 화면 | 상세 API의 서버 쪽 구간 결합 |
| 인증·미디어 | 소셜 로그인(네이버·구글)과 JWT 쿠키, 새로고침 때 로그인 상태 복구, S3 업로드와 CloudFront URL | 일반 로그인·가입·아이디/비밀번호 찾기·마이페이지, 프로필 이미지 업로드 |
| 커뮤니티 | 커뮤니티 검색, 내 커뮤니티, 통계 카드 | 서브 커뮤니티 생성·커스텀 피드, 홈 피드, 일반 글 작성·수정 폼 대부분 |
| 굿즈·결제 | — | 굿즈·장바구니·주문·결제(아임포트)·리뷰 |
Flutter 앱은 별도 레포라 이 페이지에서 다루지 않습니다.
시스템 구성
구성도는 위 커버 그림입니다. 금색이 맡은 부분이고, 웹(React)은 Spring Boot 단일 앱을 REST로 부르고, 지도는 브라우저에서 카카오 지도 SDK로 그립니다. 화살표 번호 ①–⑧은 아래 절의 번호와 같습니다.
팀 발표 자료의 구성도와는 세 군데가 다릅니다. 로그인 공급자는 네이버·구글이고, VWorld 등산로는 브라우저가 개발 프록시로 부르며(④), 예보는 서버가 받아 Redis에 씁니다.
데이터를 어디에 두었나
큐레이션 글은 두 저장소에 나뉩니다. 제목·게시판·대표 좌표 같은 공통 정보는 MySQL posts에 둡니다. 구간(지점명, 설명, 좌표, 미디어 등)은 MongoDB 문서에 두고, FK 없이 앱 코드에서 postId로 잇습니다. 경로와 설명은 처음에 MySQL 상세 표(curation_details)에 두었습니다. 2025년 6월 24일부터 26일 사이에 그 내용을 MongoDB 구간 문서로 옮겼고, 그 뒤 코드는 이 표를 쓰지 않습니다.
경로는 지점과 길이 번갈아 오는 구간입니다. 마커 N개는 구간 2N−1개(홀수 = 지점, 짝수 = 길)가 됩니다. 마지막 지점 구간이 경로 전체를 GeoJSON LineString([경도, 위도])으로 가집니다. MySQL posts.searchGeo에는 경로 좌표 평균을 "위도,경도" 문자열로 둡니다.
예보는 미리 받아 Redis에 두고, 화면 API는 Redis만 읽습니다. 키는 weather:{mountainNum}이고 12시간 뒤 만료됩니다. 날씨 출처는 6월 17일에 공공데이터포털 단기예보(격자 변환)에서 기상청 API허브 산악 지점 예보(153지점)로 바꿨습니다. 산 지점 153곳은 JSON 파일을 서버 메모리에 올려 씁니다.
지도 위 작성자는 대표 좌표 문자열로 찾습니다. searchGeo를 잘라 SQL 구면 거리 3km 이내의 글 작성자를 고릅니다. 행마다 문자열을 자르고 삼각함수를 계산하므로 인덱스를 쓰지 못합니다.
두 흐름
번호는 위 구성도의 화살표 번호와 같습니다.
산악 예보
- ① 수집.
WeatherScheduler가 153지점을 5개씩 묶어 순서대로 보내고(묶음 안은 500ms 간격), 결과를SET weather:N … EX 43200으로 씁니다. 6월 17일의for+Thread.sleep(800)을 다음 날 Reactor 파이프라인으로 바꿨습니다. - 트리거.
@Scheduled는 처음부터 주석이었습니다. 부팅 시 실행은 6월 18일에 켰다가 19일에 껐습니다. 커밋된 최종 코드에는 자동 갱신 경로가 없습니다(아래 한계 표). - ② 조회.
GET /weather/summit?mountainNum=은 Redis만 읽어 09·15·21시 값을 날짜별로 정리합니다. 값이 없으면 200과error를 돌려줍니다. - ③ 산 검색. 메모리 목록에서 이름으로 찾습니다(최대 10개).
큐레이션 글
- ⑤ 저장. 화면이
POST /posts/insert로posts행을 먼저 만들고, 받은 postId로POST /mongo/curationSegments(구간 JSON과 구간별 파일)를 보냅니다. 서버는 구간마다 파일을 S3에 올리고 CloudFront URL을 넣어 한 번에 저장합니다(saveAll). - ⑥ 검색. MongoDB
description정규식으로 postId 집합을 얻고, MySQL에서board_id = ? AND post_id IN (…)으로 글을 읽습니다. - 상세. 화면이 글과 구간(
GET /mongo/curationSegments/{postId})을 따로 불러 합칩니다. - ⑦ 지도. 반경 3km 작성자 프로필 마커를 누르면 그 글로 갑니다.
인증과 미디어
- ⑧ 소셜 로그인. OAuth2 Client로 네이버·구글 로그인을 받아 이메일로 사용자를 찾거나 만들고(닉네임 자동 생성), 액세스·리프레시 JWT를 HttpOnly 쿠키로 내립니다. 최종 화면에서 서버 흐름에 연결된 버튼은 네이버입니다.
- 요청 인증. 요청마다
JWTFilter가 헤더나 쿠키의 토큰을 검증합니다. 새로고침 때는GET /api/user로 로그인 상태를 되살립니다. - 업로드.
UUID_원본파일명키로 S3에 올리고, 6월 19일부터 CloudFront 주소를 돌려줍니다.
한계(고친다면)
| 한계 | 영향 | 고친다면 |
|---|---|---|
| 테스트가 생성기 기본 테스트뿐입니다 | 회귀를 막을 수단이 없습니다 | 예보 수집과 구간 저장부터 테스트 |
예보: 최종 코드에 자동 갱신 트리거가 없습니다. onErrorResume이 retryWhen보다 앞에 있어 재시도가 돌지 않고(실패한 산은 빈 배열이 12시간 남음), 응답 타임아웃이 없습니다 | 예보가 비거나 낡을 수 있습니다 | 스케줄 트리거, 재시도 순서 바로잡기, 타임아웃, 만료 시간과 갱신 주기 맞추기 |
큐레이션 쓰기: MySQL → S3 → MongoDB 저장을 묶는 장치가 없고(MySQL 성공 시점에 성공 알림), postId·소유자 검증이 없습니다. 구간 문서에는 @Id 필드가 없고, 수정·삭제 경로도 없습니다 | 중간에 실패하면 글과 구간이 어긋나고, 남의 글에 구간을 붙일 수 있습니다 | 한 요청으로 받아 서버가 순서와 보상 처리, 소유자 검증 |
| 조회: 반경 조회는 행마다 문자열을 파싱하고 삼각함수를 계산합니다(인덱스·LIMIT 없음). 큐레이션 검색어는 이스케이프 없이 정규식이 됩니다 | 글이 늘면 느려지고, 검색어로 정규식을 넣을 수 있습니다 | 좌표 컬럼과 범위 선필터·공간 인덱스, 검색어 이스케이프 |
| 인증·설정: 액세스 토큰이 약 20.8일(주석은 30분)이고 리프레시는 발급만 합니다. CSRF를 끈 채 쿠키로 인증하고, 토큰을 표준 출력에 찍습니다. 설정 파일에 실제 키를 커밋했고 레포가 공개였습니다(2026-09 비공개 전환, 이후 키 폐기·교체) | 토큰 탈취와 키 노출 위험 | 짧은 액세스 토큰과 리프레시, CSRF 대책, 로그 정리, 비밀 스캔 |
소스는 팀 레포라 비공개입니다. 기준 커밋은 백엔드 ec805ca, 프론트 c663906(2025-07-02)입니다.