01 — Overview · 2024.12 – 운영 중 · 1인 풀스택

Can I Pay

사내 복지포인트 사용 가능 매장을 사우분들과 익명으로 공유하는 지도 서비스. 매장 등록과 리뷰가 모두 익명이라 타부서 사우분들도 부담 없이 참여합니다.

Can I Pay 지도 화면

02Role & Scope

Role & Scope

본인이 의뢰받을 수 있는 전체 스택은 포트폴리오 사이트의 별도 섹션 참고.

03Problem

Problem

회사에 복지포인트가 도입됐지만, 정작 식사 가능한 매장 중 일부는 결제를 받지 않아 사비로 결제하는 일이 잦았습니다. "여기서 복지포인트 결제되나요?"라는 질문은 매번 사내 메신저로 흩어졌고, 정보는 한 곳에 누적되지 않았습니다.

canipay는 사우분들이 직접 매장을 등록하고 결제 가능 여부와 리뷰를 공유하는 지도 서비스로 이 문제를 해결합니다. 매장 등록과 리뷰가 모두 익명이라 타부서 사우분들도 진입장벽 없이 참여할 수 있고, 데이터는 사내에서 누적되어 새로 합류한 사람에게도 그대로 전달됩니다.

04Engineering Highlights

Engineering Highlights

1. 지도에서 결제 가능 매장 한눈에

지도 화면 — 결제 가능 매장 마커 + 클러스터크게 보기
지도 화면 — 결제 가능 매장 마커 + 클러스터

지도 위 마커 색으로 결제 가능 / 미등록 상태를 즉시 구분하고, 같은 좌표대의 마커는 클러스터로 묶어 줌 레벨과 매장 밀도에 무관하게 1~2 클릭으로 매장을 선택할 수 있도록 설계했습니다. 단일 마커일 때는 클러스터 단계를 건너뛰고 곧바로 상세 모달로 직행해 인터랙션 횟수를 줄였습니다.

2. 매장 상세 — 4축 평가로 빠른 의사결정

매장 상세 모달 — 4축 평가 카운트크게 보기
매장 상세 모달 — 4축 평가 카운트

리뷰는 자유 코멘트 외에 맛·친절·가성비·편안함의 4축 카운트로 누적합니다. 점심시간이라는 짧은 의사결정 컨텍스트에 맞춰, 사용자가 긴 텍스트를 읽지 않고도 매장의 성격을 빠르게 파악할 수 있게 만든 선택입니다.

3. 회원가입 없는 익명 매장 등록

익명 매장 등록 폼 — 회원가입 없음크게 보기
익명 매장 등록 폼 — 회원가입 없음

신규 매장 등록 흐름에서 회원가입과 로그인을 완전히 제거했습니다. 사내 도구라는 컨텍스트 위에서 "익명이라 부담 없이 등록할 수 있다"는 점이 참여 진입장벽을 가장 크게 낮춘 결정이었고, 결과적으로 한 달 만에 100+ 매장이 등록되는 데 결정적이었습니다.

4. 익명 리뷰 작성

익명 리뷰 작성 — 한 줄 + 4축 체크크게 보기
익명 리뷰 작성 — 한 줄 + 4축 체크

리뷰 작성도 동일한 익명 원칙을 따릅니다. 한 줄 코멘트 + 4축 체크만으로 완결되도록 구성해, 사우분들이 식사 직후 폰으로 빠르게 리뷰를 남길 수 있도록 했습니다.

5. 클러스터링 — 매장 밀도와 무관한 일관된 UI

클러스터: 같은 좌표대 마커 묶음크게 보기
클러스터: 같은 좌표대 마커 묶음
단일 마커: 상세 모달로 직행크게 보기
단일 마커: 상세 모달로 직행

매장이 누적되면서 줌 레벨에 따라 마커가 겹쳐 매장 선택이 어려워지는 문제가 발생했습니다. 클러스터링으로 같은 좌표대 마커를 한 점으로 묶고, 클릭 시 목록 UI를 띄워 사용자가 원하는 매장을 골라 진입하게 만들었습니다. 단일 마커는 목록 단계를 건너뛰고 상세 모달로 직행시켜 인터랙션을 최소화했습니다.

6. "지금 보고 있는 곳" 기준 위치 검색

지도 중심 좌표 기반 위치 검색크게 보기
지도 중심 좌표 기반 위치 검색

별도의 "현재 위치" 버튼 없이 지도 중심 좌표를 검색 기준으로 사용합니다. 사용자가 지도를 패닝/줌하면 그대로 "여기 주변" 결과가 갱신되어, 별도 학습 없이도 지도 인터랙션만으로 검색 흐름이 완결됩니다. 좌표계 변환(fromLonLat / toLonLat)은 libs/openlayers.ts라는 단일 경계에서만 수행하도록 격리해, OSM/Vworld 타일과 T Map 응답의 좌표 정합성 문제를 한 곳에서 다룹니다.

7. 외부 지도 API + 내부 도메인 데이터의 통합

T Map + 내부 DB 머지된 통합 검색 카드크게 보기
T Map + 내부 DB 머지된 통합 검색 카드

T Map은 "장소 검색"만 제공하고, 서비스 고유 정보(복지포인트 결제 가능 여부 · 리뷰 · 좋아요)는 내부 DB에 있습니다. Proxy 모듈을 별도 도메인으로 두고 T Map 호출을 격리한 뒤, Proxy 내부에서 응답을 Store 모델로 변환하면서 StoresService에 매장 ID로 조회해 결제상태 / 리뷰 / 좋아요 카운트를 머지합니다. 미등록 매장은 paymentStatus: 'unregistered'로 표기되어, "결과에 보이지만 아직 등록되지 않았다"는 사실 자체가 사용자에게 다음 등록을 유도하는 신호가 됩니다.

05Architecture

Architecture

시스템 구조도

시스템 구조도크게 보기
시스템 구조도

브라우저(Next.js)는 API 서버(NestJS)와 REST로 통신하고, Stores · Reviews · Proxy 세 도메인 모듈이 각각의 책임을 분담합니다. T Map 호출은 Proxy 모듈에 격리되어, 외부 장애가 등록 매장 조회로 옮겨붙지 않습니다.

06Key Decisions

Key Decisions

익명 등록·리뷰를 진입장벽 제거 수단으로 채택

맥락: 사내 도구라도 회원가입을 강제하면 타부서 사우분들의 참여가 급격히 줄어듭니다. 첫 달 안에 매장 데이터를 누적시키지 못하면 서비스 자체가 의미를 잃습니다.

옵션: 1. 사내 SSO 연동으로 식별 가능한 등록 흐름, 2. 익명 등록·리뷰 허용

선택: 2. 회원가입과 로그인을 완전히 제거하고, 매장 등록과 리뷰 작성 모두 익명으로 동작.

이유: 사내 컨텍스트 안에서 어뷰징 위험이 제한적이고, 진입장벽 제거가 사용자 수 확보 측면에서 훨씬 큰 효과를 냈습니다. 결과적으로 한 달 만에 전직원 80~100명이 합류하고 100+ 매장이 등록됐습니다.

지도 중심 좌표를 검색 기준으로 채택

맥락: 사용자는 지도에서 회사·식당의 위치를 보고 있는데, 검색이 별개 기준점으로 동작하면 결과가 의도와 어긋납니다. 또 OSM/Vworld 타일과 T Map 응답이 다른 좌표계라 좌표 정합성 문제도 발생합니다.

옵션: 1. "현재 위치" 버튼으로 사용자 GPS를 기준점화, 2. 지도 중심 좌표를 그대로 기준점으로 채택

선택: 2. 지도 인터랙션이 곧 검색 기준이 됩니다. 좌표계 변환은 libs/openlayers.ts 단일 경계에서만 수행하도록 격리.

이유: 별도 버튼/위치 권한 요청 없이도 사용자가 자연스럽게 "여기 주변" 결과를 받습니다. 좌표계 버그가 한 곳에서만 발생/수정되도록 책임 경계가 명확해집니다.

외부 API 호출을 Proxy 모듈로 격리하고 내부 데이터와 머지

맥락: T Map은 장소 검색만 제공하고, 결제 가능 여부 · 리뷰 · 좋아요는 내부 DB에 있습니다. 두 데이터를 어디서 합쳐 단일 뷰로 노출할지 결정 필요.

옵션: 1. 클라이언트에서 두 API를 따로 호출해 합치기, 2. 서버의 Proxy 모듈에서 T Map 응답을 받아 내부 데이터와 머지 후 단일 응답으로 노출

선택: 2. Proxy 모듈이 T Map 응답을 받아 Store 모델로 변환하면서 StoresService에 매장 ID로 조회해 결제상태 / 리뷰 / 좋아요를 머지.

이유: 클라이언트는 통합 응답 하나만 다루면 되어 UI 로직이 단순해집니다. T Map 장애 시에도 등록 매장 조회는 정상 작동하고, 외부 API를 다른 서비스로 교체해도 영향 범위가 Proxy 모듈에 한정됩니다.