Recent Posts
Tags
- r
- Oracle
- 오라클
- 빅데이터 분석 기반 에너지 운영 관리자 양성 및 취업과정
- 파이썬
- boostcourse
- 기초다지기
- 이기적
- 네이버부스트캠프
- boostcoures
- 이것이 취업을 위한 코딩테스트다 with 파이썬
- 정보처리기사
- 데이터베이스
- 부스트코스
- AI 플랫폼을 활용한 데이터 분석
- 데이터 분석 기반 에너지 운영 관리자 양성 및 취업과정
- Django
- PY4E
- [멀티잇]데이터 시각화&분석 취업캠프(Python)
- 빅분기
- python
- Ai
- DB
- 빅데이터분석기사
- 인공지능기초다지기
- unsub
- 코딩테스트
- SQL
- 코딩테스트 python
- 프로그래머스
- Today
- Total
매일공부
[개발 회고록/Django] 판매지원 시스템, v1.0에서 v2.1까지 본문
사내 SW 판매 프로세스 — 고객·제품·견적서·라이선스 관리를 한 곳에 모으기까지, 두 해에 걸쳐 쌓은 기능과 설계 결정을 시간순으로 정리했다.
기간 v1.0 (2026년 초) – v2.1 (2026년 7월) 스택 Django 5.2 / MariaDB / Tabler 환경 Synology NAS + Docker
이 시스템은 소프트웨어 제품의 견적 → 계약 → 라이선스 발급 → 갱신으로 이어지는 판매 프로세스 전체를 관리하기 위해 만들었다. 처음엔 고객·제품·견적서 정도만 다루는 단순한 CRUD 도구였지만, v2.0을 거치며 권한 체계·대시보드·이슈 관리·통합검색까지 갖춘 하나의 업무 시스템이 됐다. 아래는 지금까지 진행 중인 v3.0을 제외하고, v1.0~v2.1에서 실제로 완료된 것만 남긴 기록이다.
타임라인
버전별로 실제 릴리스된 기능을 정리했다. 날짜가 없는 항목은 v2.0 범위 내에서 순차적으로 진행된 작업이다.
v1.0 기본 골격 — 판매 데이터의 최소 단위를 정의하다
- 고객(
customer)·제품(product)·견적서(quotation/quo_details)·라이선스(license)·소속(belong) 등 핵심 테이블 9종을 설계하고 기본 CRUD를 구현 - Soft Delete 패턴을 전 모델의 기본 원칙으로 채택 — 실수 삭제를 막고 이력을 보존
- MariaDB 트리거 기반 히스토리 도입 — INSERT/UPDATE/DELETE 시
JSON_OBJECT로 변경 스냅샷을 이력 테이블에 자동 기록, 애플리케이션 코드가 이력 관리를 신경 쓰지 않아도 되게 함 - 견적서 4단계 워크플로우와
YY+브랜드코드+순번(예25DB001) 형태의 자동 채번 규칙 확립 - 제품 ID(
DS+C+2+T+1), 소속 그룹 ID(S001) 등 도메인별 자동생성 규칙을 코드가 아닌 데이터 규약으로 정착 - NAS 물리 폴더와 DB 레코드를 동기화하는 folders 앱 — 폴더명 생성, 삭제 시 데이터 보호 로직 포함
v1.2 2026-04-23 구조 개선 — 뒤섞여 있던 필드를 분리
- 라이선스의
degree필드를renewal_count(갱신 횟수)와duplicate_index(동일 제품 중복 소유 구분)로 분리해 의미를 명확히 함
v2.0 2026-04 ~ 08 업무 시스템으로 — 권한, 대시보드, 이슈, 검색
- 브랜드 앱 분리(04-29) — 기존
common앱에 섞여 있던SWBrand를 독립 앱으로 분리, 브랜드별 판매중지·삭제 차단 정책 추가 - RBAC 5단계 권한 체계 —
super_admin / sw_admin / sw_staff / sw_viewer / executive그룹과 4종 Mixin으로 앱 전역 접근 제어를 통일. 권한 거부 시 403 대신 경고 팝업 + 이전 페이지 리다이렉트로 사용자 경험 개선 - 대시보드 네이티브 전환 — 외부 BI 임베드 대신 Django로 직접 구현. 거래 진행 단계 KPI, 납품/결제 현황, 라이선스 만료 알림을 한 화면에
- Issue 앱(07-06 완성) — 문의·버그·AS 등 9종 유형의 이슈를 등록·배정하고 변경이력·코멘트까지 관리. 담당자는 개인 또는 그룹 단위로 배정 가능(
IssueAssignment), 견적서·라이선스 상세 페이지에 관련 이슈를 인라인으로 노출하고 대시보드에 미해결/기한초과 KPI 카드 연동 - VAT 총매출 집계 및 가격 테이블 관리(07-08), 이어서 견적서 할인 자동화(07-13) — 조건부 할인 규칙을 견적서 생성 흐름에 통합
- 이력 통계 앱 — 트리거로 쌓인 9종 히스토리 테이블을 조회하고 업무 통계로 가공
- 통합검색(08-03) — 고객·제품·견적서·라이선스를 가로지르는 검색을 별도
apps/search/로 분리 구현
v2.1 라이선스 관계망 확장
- 라이선스 갱신 체인(
LicenseRenewalLink) — 신규 발급부터 여러 차례 갱신까지 이어지는 라이선스들을 하나의 체인으로 연결 - 역할별 담당자 관리(
LicenseContact) — 소유자·사용자·관리자를 구분해 관리 - 이슈 코멘트 소프트삭제, 그룹명 관리자 편집(
GroupProfile) 등 운영 편의 기능 보강
지금의 앱 구성
위 과정을 거쳐 도메인별로 나뉜 13개 앱. 각 앱은 하나의 책임만 진다.
| belong | 소속(회사/기관) 마스터, 폐업 시 하위 고객 상태 연쇄 처리 |
| customers | 고객 담당자 CRUD, 상태코드(YY확인 등) 관리 |
| products | 제품 마스터 및 가격 이력, 날짜 기준 가격 조회 |
| quotations | 견적서 4단계 워크플로우, 만원 단위 절사 합계 계산 |
| licenses | 라이선스 생명주기, 갱신 체인, 구매 시점 스냅샷 |
| brands | SW 브랜드 마스터, 판매중지·삭제 차단 정책 |
| folders | NAS 물리 폴더 ↔ DB 레코드 동기화 |
| orders | 전 시스템 공용 상태 코드 마스터 (Hard Delete 예외) |
| issues | 이슈 등록·배정·변경이력·코멘트 |
| dashboard | 거래 KPI, 라이선스 만료, 이슈 현황 집계 |
| history | 트리거 이력 테이블 조회 및 업무 통계 |
| search | 고객·제품·견적서·라이선스 통합검색 |
| accounts | 개인계정 프로필·비밀번호, RBAC 그룹 초기화 |
돌아보며 남긴 설계 원칙
기능보다 오래 남는 것은 결정의 이유였다.
삭제는 대부분 "숨김"이다
거의 모든 모델은 Soft Delete를 기본으로 한다. 예외는
order_code·sw_brand, 그리고 견적서에 종속된 1:1 보조 테이블(DeliverySchedule, Payment 등) 뿐이며, 왜 예외인지는 모델 docstring에 각각 남겨 둔다.이력은 애플리케이션이 아니라 DB가 책임진다
MariaDB 트리거가 INSERT/UPDATE/DELETE마다 JSON 스냅샷을 이력 테이블에 남긴다. 대신 같은 인스턴스를 두 번
save()하면 트리거가 중복 발동되므로, 뷰 코드에서 이 규칙을 계속 의식해야 했다.권한은 믹스인 4개로 수렴시킨다
SWAdminRequiredMixin부터 SWViewerRequiredMixin까지, 13개 앱이 같은 4종 Mixin을 재사용한다. 새 화면을 추가할 때 권한 로직을 새로 짜지 않아도 되는 게 핵심이었다.Issue 앱의 배정은 M2M에서 조인 모델로
초기엔
Issue.assigned_to M2M으로 담당자를 연결했지만, 개인/그룹을 함께 표현할 수 없어 IssueAssignment 모델로 전면 교체했다. 스키마가 요구사항을 따라가지 못할 때는 우회하지 않고 다시 설계했다.기술 스택
| Runtime | Python 3.14.2 (Docker) |
|---|---|
| Framework | Django ≥ 5.2 |
| Database | MariaDB (salesdb_dev / salesdb, utf8mb4) |
| Frontend | Tabler (Bootstrap 5), Select2, jQuery |
| 인증 | Django Group 기반 RBAC 5단계 |
| 배포 | WhiteNoise + gunicorn, Synology NAS Docker 컨테이너 |
참고 — 지금은 v3.0(실사용 견적서 대응, 버그 수정·정책 개선)을 진행 중이다. 이 글은 그 전 단계까지, 실제로 완료된 기능만 기록한 스냅샷이다.
사내 업무 시스템 개발 기록 · 2026-08-14 기준
'IT > 작업기록' 카테고리의 다른 글
| [Django+WeasyPrint] PDF 템플릿만 CSS가 인라인인 이유 (0) | 2026.08.18 |
|---|---|
| [Git] 새 브랜치로 만든 기능, 테스트부터 dev 실반영까지 (0) | 2026.08.18 |
| [Git] dev/prod 배포 구조가 비대칭이었던 걸 온보딩 문서 쓰다가 발견한 이야기 (0) | 2026.08.12 |
| [Git CI] "Permission denied"로 임시 디렉터리 정리가 안 되던 문제 — 그리고 그 뒤에 숨어있던 진짜 원인 (0) | 2026.08.07 |
| [작업 기록] POP3 메일 체크 스크립트, 계정별 복사 대신 config로 관리하기 (0) | 2026.08.06 |
Comments