본문으로 건너뛰기
Salon CRM 체험해 보기

프로젝트 소개

네일·미용 매장의 매출, 고객, 예약을 한곳에서 관리하는 웹 서비스

사장님이 기존에 사용하던 서비스를 대체하는 것을 목표로, 기획·설계·개발·테스트·배포·운영을 혼자 맡아 만들고 실제 매장에서 쓰고 있습니다. 이 페이지는 무엇을 만들었는지, 어떤 기술을 왜 골랐는지, 어떤 과정을 거쳤는지를 한 번에 보여 드리려고 정리한 소개입니다.

로그인 없이 체험해 보기 기술 스택 보기 체험은 가짜 데이터가 든 가상의 매장이며, 방문자마다 따로 만들어지고 나중에 자동으로 지워집니다.
개발 인원
1명
시작
2026년 10월
상태
운영 중 · 실사용
마지막 배포
2026-10-23 18:57:30 KST

한눈에 보기

무엇을 만들었나

네일샵 사장님의 하루 업무 흐름(예약 확인 → 시술 후 판매 입력 → 정액권·미수 관리 → 마감)에 맞춰 구성했습니다.

매출 · 정산

서비스·정액권 판매와 미수금 회수를 한 화면에서 입력하고, 선불 정액권은 사용할 때 자동으로 차감됩니다. 일별 매출현황·내역 조회, 입력 규칙(잔액 초과·중복 입력)은 서버에서 검증합니다.

예약 달력

월·주·일·목록으로 보고, 오늘 몇 명이 오는지·노쇼, 담당자별 빈 시간과 점심 추천, 휴무·직원 쉬는 날을 보여 줍니다. 날짜 칸을 더블클릭하면 같은 화면에서 일정·예약을 등록하고 고칩니다.

고객 관리 · 이탈 방지

고객별 방문 이력·선불 잔액·미수금과, 평소 방문 간격보다 오래 오지 않은 손님을 찾아 알려 주는 재방문 점검(D-day)이 있습니다. 다른 샵에 간 것은 아닌지 확인하라는 신호입니다.

분석

일별·월별 매출 추이, 서비스·정액권별 매출, 신규 고객, 고객·선불 현황을 막대 그래프와 표로 봅니다. 기준일을 바꾸면 그날 기준으로 다시 계산합니다.

여러 매장 · 권한

한 서비스에서 매장마다 데이터가 완전히 분리되고, 관리자·점주·직원 역할에 따라 할 수 있는 일이 다릅니다. 로그인 없이 써 볼 수 있는 가상 매장 체험 모드도 있습니다.

준비 중

매출·고객 집계를 바탕으로 질문에 답하는 AI 어시스턴트, 예약 안내 문자·알림톡 자동 발송을 순서대로 붙일 계획입니다.

기술 스택과 선택 이유

유행보다 이 서비스의 조건(1인 개발, 휴대폰 업무 화면, 저사양 서버, 믿을 수 있는 숫자)에 맞는지로 골랐습니다. 대안과 트레이드오프도 함께 적었습니다.

언어·프레임워크

Java 25 (LTS)Spring Boot 4

왜 보안·데이터·검증·테스트까지 표준 구성이 갖춰져 있어 직접 만들 코드를 줄이고 업무 규칙에 집중할 수 있습니다. 최신 LTS의 레코드·스위치 식으로 데이터 객체와 분기를 짧고 안전하게 씁니다.

대안 Kotlin·Node(NestJS). 혼자 장기 운영하기에 가장 익숙하고 생태계가 넓은 쪽을 택했습니다.

빌드 도구

Gradle 9

왜 Maven이 아니라 Gradle인가 이 프로젝트의 빌드는 컴파일·테스트 말고도 CSS 빌드(Tailwind), 빌드 시각 주입, 정적 파일 이름에 빌드 시각을 붙여 브라우저 캐시 갱신 같은 맞춤 단계가 있습니다. Gradle은 빌드 스크립트가 코드라서 이런 단계를 몇 줄의 작업(task)으로 만들고 순서를 선언할 수 있고, 증분 빌드와 캐시로 매번 CI 시간도 줄어듭니다.

트레이드오프 Maven은 선언적이라 표준화가 쉽고 처음 보는 사람도 읽기 쉽습니다. 팀이 크고 빌드가 단순하면 Maven이 더 합리적일 수 있습니다.

화면

Thymeleaf (서버 렌더링)Tailwind CSS바닐라 JavaScript

왜 SPA가 아닌가 업무 화면은 휴대폰에서 빨리 열리고 단순해야 합니다. 서버 렌더링은 화면과 규칙이 한곳에 있어 검증이 쉽고, 템플릿이 글자를 자동으로 이스케이프해 XSS를 구조적으로 막아 줍니다. 달력 창·날짜 고르기처럼 움직임이 필요한 곳만 작은 JS로 보강하고, 프레임워크 번들은 쓰지 않았습니다. 업무 규칙은 화면과 분리된 서비스 계층에 있어서 나중에 모바일 앱·API가 필요해져도 재사용할 수 있습니다.

Tailwind 사용한 클래스만 빌드해 CSS가 작고, 디자인 일관성을 유틸리티 규칙으로 유지합니다.

데이터

MariaDBJPA / HibernateFlywayH2 (테스트)

왜 MySQL 호환이면서 가벼워서 작은 서버에 운영·개발 DB를 따로 올릴 수 있습니다. 스키마는 Flyway 마이그레이션으로 버전 관리하고, 모든 테이블·컬럼에 설명(COMMENT)을 달지 않으면 테스트가 실패하게 해서 DB 문서가 코드와 같이 갑니다. 테스트는 같은 마이그레이션을 메모리 DB(H2)에 적용해 DB 없이도 전체를 빠르게 돌립니다.

한계 H2로는 운영 DB 고유의 락·격리 수준 동작을 확인할 수 없어, 그 부분은 별도 점검 대상으로 두었습니다.

인증·보안

Spring Security

왜 세션 로그인, CSRF, 역할별 접근, 로그인 실패 잠금을 검증된 방식으로 씁니다. 모든 쓰기 요청의 CSRF 토큰, 계정·비밀번호 변경 시 열린 세션 무효화, 요청마다 남기는 감사 기록(누가·무엇을)을 기본값으로 둡니다. 개인정보는 로그에 남기지 않는 것을 규칙으로 하고, 외부 AI에는 개인을 식별할 수 없는 집계 숫자만 보내는 원칙을 정했습니다.

배포·운영

DockerGitHub ActionsNginx Proxy ManagerLet's Encrypt

왜 앱·DB·프록시를 컨테이너로 나눠 한 부분의 고장이 번지지 않게 하고, 푸시 한 번으로 빌드·테스트·배포가 이어집니다. 빌드는 CI(GitHub의 컴퓨터)가 하고 서버는 완성품을 받아 교체만 합니다. 시작 확인에 실패하면 직전 버전으로 자동 복구합니다. HTTPS 인증서는 무료·자동 갱신입니다.

대안 Kubernetes·관리형 클라우드. 이 규모에서는 운영 비용과 복잡도가 이득보다 커서 단일 서버 컨테이너로 시작했습니다.

품질 도구

JUnit 5MockMvcjsoup

왜 화면은 실제 HTML을 받아 jsoup으로 읽고 검증해서 "버튼이 있는가"가 아니라 "눌렀을 때 저장되고 돌아오는가"까지 확인합니다. 패키지 구조·템플릿 규칙·스키마 설명 규칙도 테스트로 강제합니다.

데이터 이전 도구

PythonPlaywright

왜 기존에 사용하던 서비스는 내보내기 기능이 약해서, 화면을 읽기만 하는 수집기(데이터를 바꾸는 요청은 서버에 닿기 전에 차단)로 이력을 모았습니다. 서비스 본체와 분리된 도구라 스택이 달라도 문제가 없고 언어도 수집에 맞는 쪽을 골랐습니다. 지금은 이 수집기를 관리자가 버튼을 누를 때만 도는 별도 컨테이너로 묶어, 브라우저가 메모리를 많이 써도 서비스에 영향이 없게 하고 가져오기는 하나의 트랜잭션으로 처리합니다(중간에 실패하면 전부 되돌림).

전체 구조

사용자가 보는 흐름(위)과 코드가 배포되는 흐름(아래)입니다. 내부 주소·포트·계정 같은 값은 일부러 그리지 않았습니다.

Salon CRM 전체 구조도 사용자의 브라우저가 HTTPS로 리버스 프록시를 거쳐 앱 컨테이너에 접속하고, 앱은 화면, 서비스, 저장소 계층을 거쳐 데이터베이스를 쓴다. 개발자가 코드를 올리면 CI가 빌드와 테스트를 하고 서버에 배포하며 실패하면 자동으로 복구한다. 사용자 휴대폰 · PC 브라우저 리버스 프록시 HTTPS 종료 · 인증서 자동 갱신 앱 컨테이너 (Spring Boot) 화면 Thymeleaf · JS 서비스 업무 규칙 · 검증 저장소 JPA · 업장 필터 보안 · 감사 · 업장 분리 로그인 · CSRF · 역할 · 기록 — 모든 요청이 거쳐 감 운영 DB MariaDB · 외부 비공개 개발자 코드 푸시 CI (GitHub Actions) 빌드 · 전체 테스트 서버 배포 컨테이너 교체 → 상태 확인 → 실패 시 자동 복구 개발 DB (운영과 분리) 개발 PC에서는 암호화된 통로로만 접속 데이터 이전 도구 (Python) 읽기 전용 수집 → 미리보기 → 저장 백업 · 감사 기록 일일 백업 · 요청 기록 장기 보관

설계에서 중요하게 본 것

  1. 1. 숫자는 한 번만 계산한다 (파생 값은 저장하지 않음)

    선불 잔액·미수금·방문 횟수는 판매 기록(원장)에서 그때그때 계산합니다. 같은 값이 두 곳에 있어 어긋나는 문제를 없애고 "그날 기준" 조회도 자연스럽게 됩니다. 규모가 커지면 요약 테이블·캐시를 더하는 순서로 확장할 계획입니다.

  2. 2. 데이터 이전은 되돌릴 수 있고 검증된다

    이전 코드는 여러 번 실행해도 같은 결과(멱등)이고, 항상 "미리보기 → 확인 → 저장" 순서입니다. 오류가 하나라도 있으면 아무것도 저장하지 않습니다. 이전한 합계는 기존에 사용하던 서비스의 엑셀 값과 대조해 일치율로 확인했습니다.

  3. 3. 규칙은 문서가 아니라 테스트로 강제한다

    패키지 계층, 모든 화면의 공통 구성(반응형·저작권), 모든 컬럼의 설명, 모든 메뉴 화면의 도움말을 테스트가 검사합니다. 새로 만든 코드가 약속을 어기면 CI에서 바로 실패합니다.

  4. 4. 사용자가 일하는 흐름을 먼저 본다

    실제 사장님의 요청에서 출발해 "설정하러 다른 화면으로 갔다 오는 불편"을 없앴습니다(달력 칸 더블클릭 → 그 자리에서 등록·수정·삭제). 휴대폰에서는 큰 터치 영역과 단순한 입력을 우선하고, 처음 로그인 안내와 화면별 도움말을 넣었습니다.

  5. 5. 보안은 기본값과 기록으로

    역할별 권한, CSRF, 세션 보호, 로그인 잠금, 요청 감사 기록, 개인정보를 로그에 남기지 않는 규칙을 처음부터 넣었습니다. 외부 AI 서비스를 쓸 때도 개인을 식별할 수 있는 정보는 보내지 않는 수준(집계 숫자)부터 시작하기로 정했습니다.

  6. 6. 처음 보는 사람도 써 볼 수 있게

    로그인 없이 가짜 데이터가 든 가상 매장을 방문자마다 따로 만들어 주고 자동으로 정리합니다. 이 소개 페이지의 "체험해 보기"가 그 입구이고, 체험 중에도 실제 매장 데이터와는 완전히 분리됩니다.

만들어 온 과정

  1. 1주차 · 서버와 배포 틀 만들기

    작은 클라우드 서버에 Docker로 앱·DB·프록시를 올리고 도메인과 HTTPS를 연결했습니다. 처음에는 서버 안에서 Jenkins로 빌드했지만 메모리가 모자라 한 번에 25분이 넘게 걸려서, 빌드는 GitHub Actions가 하고 서버는 완성품만 교체하는 구조로 바꿔 배포 시간을 몇 분으로 줄였습니다. 운영과 개발 DB를 분리하고 개발 PC에서는 암호화된 통로로만 연결합니다.

  2. 2주차 · 기존 데이터를 옮길 길 만들기

    기존에 사용하던 서비스의 화면을 읽기만 하는 수집기를 만들고(데이터를 바꾸는 요청은 차단), 고객·서비스·매출 이력을 가져오는 멱등 가져오기를 구현했습니다. 이전한 합계를 엑셀과 대조해 차이가 나는 원인(이월 잔액)을 찾아 보정 구조를 설계했습니다.

  3. 2~3주차 · 업무 기능과 여러 매장 구조

    판매 입력·고객·미수·선불·분석 화면을 다시 만들고, 로그인과 업장 분리(멀티테넌시)를 넣었습니다. 사용 조건(역할)별로 보이는 메뉴와 입력 칸이 달라지고, 모든 화면에 반응형 레이아웃·도움말·의견 보내기를 갖췄습니다.

  4. 3주차 · 예약, 이탈 방지, 체험 모드

    예약 달력(월·주·일·목록), 빈 시간·점심 추천, 휴무·쉬는 날, 재방문 D-day를 만들고, 사용자의 의견을 받아 달력 한 화면에서 일정·예약을 바로 등록·수정하도록 고쳤습니다. 로그인 없이 써 보는 가상 매장 체험 모드도 이때 추가했습니다.

  5. 운영 보안 강화와 복구 절차 문서화

    운영 중 보안 점검을 하면서 접근 통제와 감사 기록을 강화하고, 서버를 처음부터 다시 만들어도 같은 환경이 나오도록 구축 절차를 문서로 남겼습니다. 비밀값 교체와 데이터 재적재까지 한 번에 따라 할 수 있습니다.

  6. 지금 · 안정화와 다음 기능 준비

    코드·보안·정합성을 병렬로 점검하는 총체 점검을 해서 보완 목록을 만들고 우선순위대로 처리하고 있습니다. 이 서비스의 큰 목표인 AI 어시스턴트를 그 위에 얹을 준비 단계입니다.

문제를 풀어 간 방식

증상 → 원인 → 해결 → 재발 방지 순서로 기록해 둔 사례입니다.

배포 25분 → 몇 분

원인 메모리가 작은 서버에서 빌드까지 하느라 스왑이 폭주했습니다. 해결 빌드·테스트는 CI로 옮기고 서버는 이미지 교체와 상태 확인만 하게 했습니다. 보강 새 버전이 뜨지 않으면 직전 이미지로 자동 복구합니다.

메뉴의 ▾ 가 가끔 â–¾ 로 깨지던 문제

추적 같은 파일인데 가끔만 깨져서 응답 헤더를 비교했더니 CSS·JS 응답에 글자 인코딩(charset)이 없었고, 브라우저가 가끔 다른 인코딩으로 추측했습니다. 해결 정적 파일 응답에 UTF-8을 명시하는 필터와 CSS 이스케이프로 근본 원인을 막고, 재발 방지 응답 헤더를 검사하는 테스트를 추가했습니다.

이전한 숫자가 맞는지 증명하기

고객 목록 엑셀의 총매출·판매 횟수·미수금과 우리 계산을 고객별로 대조해 대부분 일치함을 확인했고, 어긋난 고객들의 공통 원인(이월·수기 보정)을 찾아 보정 기록 구조로 해결했습니다. 동명이인은 방문일·미수·횟수·총매출의 조합으로 구별했습니다.

"한 화면에서 끝내 달라"는 요청

휴무일·쉬는 날을 등록하고 달력에서 확인하려면 화면을 오가야 한다는 점주의 불편을 받아, 달력 칸 더블클릭 창에서 등록·수정·삭제하고 처리 뒤 보던 달력으로 돌아오게 했습니다. 이동 주소는 사이트 안의 예약 화면 경로만 허용해 악용을 막았습니다.

품질과 운영

테스트 전략

  • 업무 규칙(잔액·미수·재방문 계산)은 경계값까지 단위 테스트
  • 화면은 실제 HTML을 받아 요소·문구·이동·저장 결과를 검사
  • 보안(역할별 접근, 로그인 잠금, 업장 분리, CSRF)을 시나리오로 검사
  • 구조·스키마·템플릿 규칙을 테스트로 강제
  • 모든 푸시마다 전체 테스트가 돌고, 실패하면 배포하지 않음

운영

  • 자동 배포 + 상태 확인 + 실패 시 자동 복구
  • 일일 백업과 복구 절차 문서, 요청 감사 기록 장기 보관
  • 느린 요청·느린 쿼리 기록, 컨테이너 로그 크기 제한
  • 비밀값은 코드 저장소에 두지 않고, 컨테이너는 일반 사용자 권한으로 실행
  • 사용자 의견 수집 창으로 문제와 요청이 개발자에게 바로 전달

앞으로의 계획과 개선할 점

AI 어시스턴트

모델이 SQL을 직접 쓰지 않고, 서버가 검증해 둔 "집계 도구"(기간별 매출, 서비스별 건수, 재방문율 등)를 골라 호출하게 합니다. 서버가 계산한 숫자만 모델에 전달하므로 답이 화면의 숫자와 일치하고 개인정보가 나가지 않으며, 질문과 호출 기록을 남깁니다.

솔직한 개선 과제

  • 동시 입력·중복 제출 방어(낙관적 락) 강화
  • 고객 수가 수천 명이 되면 집계 캐시·요약 테이블 도입
  • 운영 DB와 같은 엔진으로 하는 통합 테스트 추가
  • 예약 안내 문자·알림톡 자동 발송

개발 방식 요구사항은 실제 사용자(매장 사장님)와 주고받으며 짧은 주기로 반영했고, AI 코딩 도구(Claude Code)를 설계 검토·테스트 작성·점검에 보조로 활용했습니다. 무엇을 만들지와 설계 결정, 검증, 운영 판단은 직접 합니다.

이 페이지는 설명용 요약이며, 계정·주소·설정값 같은 민감한 정보는 포함하지 않았습니다. 수치는 대략의 값입니다.