로컬푸드 직매장 정산 자동화 데이터 구조 문서
1. 설계 범위와 기준
이 문서는 첫 버전에서 실제로 사용하는 데이터를 정의합니다. 대상은 하나의 로컬푸드 직매장입니다. 여러 매장 통합 관리는 포함하지 않습니다.
- 정산 주차별 판매 원장과 정산서를 관리합니다.
- 농가·품목별로 다른 정산 규칙과 적용 기간을 보관합니다.
- 마감 완료된 정산 주차의 계산 근거와 금액은 바꾸지 않습니다.
- 마감 후 오류는 원 정산서를 수정하지 않고 다음 정산 주차의 정정 항목으로 반영합니다.
- 농가는 자기 농가의 정산서와 이의 제기만 조회할 수 있습니다.
- 실제 은행 이체와 은행 이체용 지급 파일 생성은 SRS 첫 개발 범위에서 제외되어 있습니다. 다만 정산서의 지급액과 입금 예정일은 저장합니다.
가정: 데이터베이스는 PostgreSQL을 사용하고, Prisma ORM으로 접근합니다. 금액은 부동소수점이 아닌 정수 원 단위로 저장합니다. (NFR-001)
2. 주요 데이터와 책임
| 데이터 표 | 사업 용어 | 저장 목적 | 관련 요구사항 |
|---|---|---|---|
users | 운영자·농가 로그인 계정 | 로그인, 계정 상태, 역할과 농가 연결을 관리 | FR-001, FR-002, FR-003, FR-020~023 |
farmers | 농가 | 농가 기본 정보와 입점 상태를 관리 | FR-003, FR-005, FR-016, FR-020 |
product_groups | 품목군 | 품목을 분류하고 품목군 정산 규칙을 적용 | FR-005, FR-006, FR-012 |
products | 품목 | 농가, 품목군, 품목명, 규격, 사용 상태를 관리 | FR-005, FR-008, FR-010~012 |
product_code_mappings | 상품코드 연결 | 계산대 판매 파일의 상품코드를 품목에 연결하고 연결 이력을 보관 | FR-005, FR-008 |
settlement_rules | 정산 규칙 | 수수료율, 할인 수수료, 반품·폐기 부담과 적용 기간을 관리 | FR-006, FR-010~012 |
settlement_weeks | 정산 주차 | 판매 기간, 목요일 마감일, 입금 예정일, 정산 상태를 관리 | FR-004, FR-015~019 |
cashier_sales_files | 계산대 판매 파일 | 원본 파일 정보, 업로드 이력과 처리 상태를 보관 | FR-007, FR-009, FR-014 |
cashier_sales_file_rows | 계산대 판매 파일 원본 행 | 업로드 파일의 원문 행과 형식 오류를 보존 | FR-007, FR-009, FR-010 |
sales_ledgers | 판매 원장 | 판매·할인·반품과 폐기를 정산 근거로 기록 | FR-009~014 |
disposals | 폐기 | 유통기한 경과·미회수 재고 폐기와 부담 금액을 기록 | FR-011~014 |
settlement_errors | 정산 오류 | 마감을 막는 미연결, 규칙 누락, 중복, 대조 오류를 관리 | FR-013~015, FR-017 |
settlements | 정산 | 정산 주차별 농가의 계산 결과와 계산 시점 복사본을 저장 | FR-012, FR-014, FR-016 |
settlement_statements | 정산서 | 농가에 제공할 정산서와 생성·마감 시점의 결과를 저장 | FR-016, FR-017, FR-020~022 |
correction_items | 정정 항목 | 마감 후 오류를 원 정산서와 다음 정산 주차에 연결 | FR-018, FR-019 |
disputes | 이의 제기 | 농가 이의 내용, 처리 결과와 안내 기록을 관리 | FR-022, FR-023 |
evidences | 증빙 | 폐기와 이의 제기에 첨부한 사진의 메타데이터를 보관 | FR-011, FR-022, FR-023 |
reconciliations | 대조 | 매장 매출과 정산 합계의 대조 결과를 저장 | FR-014, FR-015, FR-017 |
activity_logs | 주요 작업 이력 | 로그인, 규칙 변경, 파일 업로드, 마감, 정정 처리의 이력을 보관 | FR-002, FR-006~009, FR-017~019, FR-023, FR-024 |
3. 데이터 관계도
그림을 그리는 중…
이 관계도에서 settlement_weeks가 주간 업무의 중심입니다. 계산대 판매 파일, 판매 원장, 폐기, 정산 오류, 대조와 농가별 정산은 모두 하나의 정산 주차에 연결됩니다.
마감 후에도 과거 계산 근거를 확인할 수 있도록 판매 원장과 정산에는 당시 적용된 정산 규칙과 계산 결과의 복사본을 저장합니다. (FR-012, FR-017, NFR-003)
4. 공통 저장 원칙
4.1 금액과 날짜
| 항목 | 저장 방식 | 이유 | 관련 요구사항 |
|---|---|---|---|
| 금액 | Int 원 단위 | 소수점 오차 없이 수수료와 지급액을 계산 | FR-012, FR-014, NFR-001 |
| 수수료율 | Decimal(7,4) | 예: 10%, 15%, 개별 약정 비율 저장 | FR-006, FR-012 |
| 판매일·마감일·입금 예정일 | DateTime의 날짜 기준 또는 @db.Date | 정산 주차와 적용 규칙을 판매일 기준으로 판단 | FR-004, FR-006, NFR-002 |
| 작업 시점 | DateTime | 업로드, 마감, 정정, 이의 처리 시점을 추적 | FR-007, FR-017~019, FR-023, FR-024 |
4.2 삭제와 사용 중지
- 농가, 품목, 품목군, 로그인 계정은 정산 이력이 생긴 뒤 물리적으로 삭제하지 않습니다.
- 대신
isActive,status,deactivatedAt등으로 사용 중지 처리합니다. - 마감된 정산 주차와 연결된 판매 원장, 정산 규칙, 정산서, 정정 항목은 수정·삭제하지 않습니다. (FR-002, FR-005, FR-006, FR-017, NFR-003)
4.3 개인정보 최소화
첫 버전에서 농가에 필요한 정보는 다음으로 제한합니다.
- 농가명
- 농가 로그인용 아이디 또는 이메일
- 비밀번호 해시값
- 계정 상태
확인 필요: 농가 연락처, 사업자등록번호, 계좌번호는 첫 버전의 정산 조회·정산서 생성에 필수 정보가 아닙니다. 실제 이체 또는 지급 파일 생성 기능을 추가할 때 별도 보관 필요성과 접근 권한을 검토해야 합니다.
5. 표별 필드 정의
5.1 로그인 계정과 권한
users — 운영자·농가 로그인 계정
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 계정 식별값 | FR-001~003 |
loginId | 문자열 | 예 | 로그인 아이디, 전체에서 중복 불가 | FR-001, FR-003 |
passwordHash | 문자열 | 예 | 암호화된 비밀번호 값 | FR-001, NFR-005 |
role | 열거형 | 예 | STORE_OWNER, SETTLEMENT_MANAGER, CLOSING_MANAGER, FARMER | FR-001, FR-002 |
farmerId | UUID | 아니오 | 농가 계정인 경우 연결되는 농가 | FR-003, FR-020 |
status | 열거형 | 예 | ACTIVE, SUSPENDED | FR-002, FR-003 |
lastLoginAt | 날짜·시간 | 아니오 | 최근 로그인 성공 시점 | FR-001, FR-024 |
createdAt | 날짜·시간 | 예 | 생성 시점 | FR-002, FR-024 |
updatedAt | 날짜·시간 | 예 | 마지막 변경 시점 | FR-002, FR-024 |
제약 조건
role = FARMER이면farmerId가 반드시 있어야 합니다.- 하나의 농가는 하나의 활성 농가 계정에만 연결합니다.
role = FARMER이 아닌 계정에는farmerId를 저장하지 않습니다.- 계정 중지 상태에서는 새 로그인 세션을 만들 수 없습니다. (FR-001~003, NFR-004, NFR-005)
5.2 농가·품목·상품코드
farmers — 농가
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 농가 식별값 | FR-003, FR-005 |
name | 문자열 | 예 | 농가명 또는 정산서 표시 이름 | FR-005, FR-016 |
isActive | 불리언 | 예 | 현재 입점·사용 가능 여부 | FR-005 |
deactivatedAt | 날짜·시간 | 아니오 | 사용 중지 시점 | FR-005 |
createdAt | 날짜·시간 | 예 | 등록 시점 | FR-005 |
updatedAt | 날짜·시간 | 예 | 변경 시점 | FR-005 |
product_groups — 품목군
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 품목군 식별값 | FR-005, FR-006 |
name | 문자열 | 예 | 예: 채소, 가공식품, 계란 | FR-005, FR-006 |
isActive | 불리언 | 예 | 사용 여부 | FR-005 |
createdAt | 날짜·시간 | 예 | 등록 시점 | FR-005 |
products — 품목
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 품목 식별값 | FR-005, FR-008 |
farmerId | UUID | 예 | 품목을 출하하는 농가 | FR-005 |
productGroupId | UUID | 아니오 | 소속 품목군 | FR-005, FR-006 |
name | 문자열 | 예 | 품목명 | FR-005, FR-010 |
specification | 문자열 | 아니오 | 규격, 예: 1봉, 10개입 | FR-005 |
isActive | 불리언 | 예 | 판매·신규 연결 가능 여부 | FR-005 |
deactivatedAt | 날짜·시간 | 아니오 | 사용 중지 시점 | FR-005 |
createdAt | 날짜·시간 | 예 | 등록 시점 | FR-005 |
updatedAt | 날짜·시간 | 예 | 변경 시점 | FR-005 |
product_code_mappings — 상품코드 연결
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 연결 식별값 | FR-005, FR-008 |
productCode | 문자열 | 예 | 계산대 판매 파일의 상품코드 | FR-005, FR-008 |
productId | UUID | 예 | 연결된 품목 | FR-005, FR-008 |
mappingSource | 열거형 | 예 | MANUAL, AUTO_CONFIRMED 등 연결 방식 | FR-008 |
isActive | 불리언 | 예 | 현재 자동 연결에 사용할지 여부 | FR-008 |
createdById | UUID | 예 | 직접 연결 또는 등록 담당자 | FR-008, FR-024 |
createdAt | 날짜·시간 | 예 | 연결 시점 | FR-008, FR-024 |
endedAt | 날짜·시간 | 아니오 | 연결 종료 시점 | FR-008 |
같은 상품코드는 동시에 둘 이상의 사용 중인 품목에 연결할 수 없습니다. 직접 연결을 변경해도 과거 연결 기록은 남기며, 이후 판매에만 새 연결을 사용합니다. (FR-005, FR-008)
5.3 정산 규칙
settlement_rules — 정산 규칙
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 정산 규칙 식별값 | FR-006, FR-012 |
name | 문자열 | 예 | 운영자가 알아볼 규칙 이름 | FR-006 |
farmerId | UUID | 아니오 | 특정 농가 대상 | FR-006 |
productId | UUID | 아니오 | 특정 품목 대상 | FR-006 |
productGroupId | UUID | 아니오 | 특정 품목군 대상 | FR-006 |
commissionRate | Decimal | 예 | 수수료율. 예: 0.1000은 10% | FR-006, FR-012 |
discountCommissionBasis | 열거형 | 예 | BEFORE_DISCOUNT, AFTER_DISCOUNT | FR-006, FR-012 |
returnBurdenParty | 열거형 | 예 | FARMER, STORE, NONE | FR-006, FR-012 |
disposalBurdenParty | 열거형 | 예 | FARMER, STORE, NONE | FR-006, FR-011~012 |
disposalCalculationMethod | 열거형 | 예 | MANUAL_AMOUNT, ORIGINAL_PRICE, ACTUAL_COST | FR-006, FR-011 |
effectiveFrom | 날짜 | 예 | 적용 시작일 | FR-006, FR-012 |
effectiveTo | 날짜 | 아니오 | 적용 종료일 | FR-006 |
isConfirmed | 불리언 | 예 | 농가·품목별 약정이 확정됐는지 여부 | FR-006 |
createdById | UUID | 예 | 등록 담당자 | FR-006, FR-024 |
createdAt | 날짜·시간 | 예 | 등록 시점 | FR-006, FR-024 |
규칙 적용 우선순위
판매 원장의 품목과 농가, 판매일을 기준으로 아래 순서의 규칙 하나를 선택합니다.
- 농가 + 개별 품목
- 농가 + 품목군
- 농가 전체
- 개별 품목
- 품목군
- 농가·품목·품목군이 없는 매장 공통 규칙
effectiveFrom ≤ 판매일 ≤ effectiveTo 조건을 만족해야 하며, 종료일이 없으면 계속 유효한 규칙입니다. 같은 우선순위에서 둘 이상의 규칙이 나오면 정산 오류를 생성합니다. (FR-006, FR-012, FR-013)
5.4 정산 주차와 계산대 판매 파일
settlement_weeks — 정산 주차
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 정산 주차 식별값 | FR-004~019 |
periodStartDate | 날짜 | 예 | 판매 기간 시작일 | FR-004 |
periodEndDate | 날짜 | 예 | 판매 기간 종료일 | FR-004 |
closingDate | 날짜 | 예 | 목요일 정산 마감일 | FR-004, FR-017 |
expectedDepositDate | 날짜 | 예 | 농가에 표시할 입금 예정일 | FR-004, FR-020 |
status | 열거형 | 예 | 정산 진행 상태 | FR-004, FR-015~018 |
closedAt | 날짜·시간 | 아니오 | 실제 마감 완료 시점 | FR-017 |
closedById | UUID | 아니오 | 마감 권한자 | FR-017, FR-024 |
createdById | UUID | 예 | 정산 주차 생성 담당자 | FR-004 |
createdAt | 날짜·시간 | 예 | 생성 시점 | FR-004 |
status 값은 다음을 사용합니다.
PREPARING: 준비 중PROCESSING: 처리 중HAS_ERRORS: 오류 있음READY_TO_CLOSE: 마감 가능STATEMENTS_GENERATED: 정산서 생성CLOSED: 마감 완료CORRECTION_PENDING: 정정 반영 예정
CLOSED 상태에서는 판매 원장, 폐기, 정산 규칙 적용 결과, 정산 및 정산서를 변경하지 않습니다. (FR-017, NFR-003)
cashier_sales_files — 계산대 판매 파일
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 파일 식별값 | FR-007, FR-009 |
settlementWeekId | UUID | 예 | 대상 정산 주차 | FR-007 |
originalFileName | 문자열 | 예 | 업로드 당시 파일명 | FR-007 |
storageKey | 문자열 | 예 | 원본 파일 저장 경로 또는 객체 저장소 키 | FR-007, NFR-006 |
fileHash | 문자열 | 예 | 같은 파일 중복 업로드 검사값 | FR-007 |
fileFormat | 열거형 | 예 | 첫 버전 지원 형식 예: CSV, XLSX | FR-007 |
status | 열거형 | 예 | UPLOADED, VALIDATING, PARTIALLY_PROCESSED, PROCESSED, FAILED | FR-007 |
totalRowCount | 정수 | 예 | 전체 행 수 | FR-007 |
validRowCount | 정수 | 예 | 정상 행 수 | FR-007 |
errorRowCount | 정수 | 예 | 오류 행 수 | FR-007 |
uploadedById | UUID | 예 | 업로드 담당자 | FR-007, FR-024 |
uploadedAt | 날짜·시간 | 예 | 업로드 시점 | FR-007 |
processedAt | 날짜·시간 | 아니오 | 처리 완료 시점 | FR-007 |
cashier_sales_file_rows — 계산대 판매 파일 원본 행
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 원본 행 식별값 | FR-007, FR-009 |
cashierSalesFileId | UUID | 예 | 소속 계산대 판매 파일 | FR-007 |
rowNumber | 정수 | 예 | 원본 파일 행 번호 | FR-007, FR-010 |
rawData | JSON | 예 | 원본 열과 값을 그대로 보관 | FR-007, FR-010 |
transactionExternalId | 문자열 | 아니오 | 계산대 거래 고유값 | FR-009 |
saleDate | 날짜·시간 | 아니오 | 파일에서 읽은 판매일 | FR-007 |
productCode | 문자열 | 아니오 | 파일의 상품코드 | FR-008 |
productNameRaw | 문자열 | 아니오 | 파일의 원본 품목명 | FR-008 |
validationStatus | 열거형 | 예 | VALID, INVALID, PENDING_REVIEW | FR-007 |
validationMessage | 문자열 | 아니오 | 열 누락, 날짜 범위 오류 등의 설명 | FR-007 |
createdAt | 날짜·시간 | 예 | 저장 시점 | FR-007 |
원본 행은 수정하지 않습니다. 운영자가 직접 상품을 연결하거나 오류를 처리해도 원본 파일 내용과 원본 행은 그대로 보존합니다. (FR-007, FR-008, NFR-006)
5.5 판매 원장과 폐기
sales_ledgers — 판매 원장
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 판매 원장 식별값 | FR-009~014 |
settlementWeekId | UUID | 예 | 정산 대상 주차 | FR-009 |
cashierSalesFileRowId | UUID | 아니오 | 원본 파일 행. 수기 조정은 비어 있을 수 있음 | FR-009, FR-014 |
farmerId | UUID | 아니오 | 연결된 농가 | FR-008, FR-010 |
productId | UUID | 아니오 | 연결된 품목 | FR-008, FR-010 |
transactionType | 열거형 | 예 | SALE, DISCOUNT_SALE, RETURN, MANUAL_ADJUSTMENT | FR-009, FR-010, FR-014 |
transactionDate | 날짜·시간 | 예 | 거래일 | FR-009, FR-012 |
externalTransactionId | 문자열 | 아니오 | 계산대 거래 고유값 | FR-009 |
quantity | Decimal | 예 | 거래 수량 | FR-009, FR-010 |
regularSaleAmount | 정수 | 예 | 할인 전 정상 판매금액 | FR-010, FR-012 |
actualSaleAmount | 정수 | 예 | 할인 후 실제 판매금액 또는 반품 차감 전 기준값 | FR-010, FR-012 |
discountAmount | 정수 | 예 | 정상 판매금액과 실제 판매금액 차이 | FR-010 |
returnAmount | 정수 | 예 | 반품·취소 차감액 | FR-010, FR-012 |
netSalesAmount | 정수 | 예 | 정산에 반영할 순 판매액 | FR-010, FR-012 |
mappingStatus | 열거형 | 예 | AUTO_MAPPED, MANUALLY_MAPPED, UNMAPPED, CONFLICTED | FR-008, FR-013 |
settlementStatus | 열거형 | 예 | PENDING, CALCULATED, ERROR, EXCLUDED | FR-010, FR-013 |
appliedRuleId | UUID | 아니오 | 실제 적용한 정산 규칙 | FR-012 |
appliedRuleSnapshot | JSON | 아니오 | 계산 당시 수수료·부담 기준 복사본 | FR-012, FR-017 |
commissionBaseAmount | 정수 | 아니오 | 수수료 계산 기준 금액 | FR-012 |
commissionAmount | 정수 | 아니오 | 계산된 수수료 | FR-012 |
farmerReturnBurdenAmount | 정수 | 예 | 농가 부담 반품 금액 | FR-012 |
createdAt | 날짜·시간 | 예 | 생성 시점 | FR-009 |
updatedAt | 날짜·시간 | 예 | 마감 전 마지막 처리 시점 | FR-008~013 |
disposals — 폐기
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 폐기 식별값 | FR-011~014 |
settlementWeekId | UUID | 예 | 대상 정산 주차 | FR-011 |
farmerId | UUID | 예 | 대상 농가 | FR-011 |
productId | UUID | 예 | 대상 품목 | FR-011 |
disposalType | 열거형 | 예 | EXPIRATION, UNCLAIMED_STOCK, OTHER | FR-011 |
reasonDetail | 문자열 | 아니오 | 구체적 폐기 사유 | FR-011 |
quantity | Decimal | 예 | 폐기 수량 | FR-011 |
burdenParty | 열거형 | 예 | FARMER, STORE, NONE | FR-011 |
settlementRuleId | UUID | 아니오 | 적용 규칙 | FR-011, FR-012 |
settlementRuleSnapshot | JSON | 아니오 | 적용 당시 부담 기준 복사본 | FR-011, FR-017 |
settlementImpactAmount | 정수 | 예 | 정산 반영 금액 | FR-011, FR-012 |
notifiedFarmerAt | 날짜·시간 | 아니오 | 농가 안내 날짜 | FR-011 |
notificationMethod | 문자열 | 아니오 | 예: 전화, 대면 전달 | FR-011 |
handledById | UUID | 예 | 처리 담당자 | FR-011, FR-024 |
handledAt | 날짜·시간 | 예 | 처리 시점 | FR-011 |
createdAt | 날짜·시간 | 예 | 등록 시점 | FR-011 |
폐기에 적용할 정산 규칙이 없으면 settlementImpactAmount를 자동 확정하지 않고 정산 오류로 남깁니다. 농가 부담 폐기이면서 안내 기록이 없으면 경고를 표시합니다. (FR-011, FR-013)
5.6 정산 오류와 대조
settlement_errors — 정산 오류
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 오류 식별값 | FR-013~015 |
settlementWeekId | UUID | 예 | 오류가 발생한 정산 주차 | FR-013 |
salesLedgerId | UUID | 아니오 | 관련 판매 원장 | FR-013 |
disposalId | UUID | 아니오 | 관련 폐기 | FR-013 |
errorType | 열거형 | 예 | 오류 유형 | FR-013 |
severity | 열거형 | 예 | BLOCKING, WARNING | FR-013, FR-017 |
status | 열거형 | 예 | OPEN, RESOLVED | FR-013 |
message | 문자열 | 예 | 오류 내용 | FR-013 |
resolutionNote | 문자열 | 아니오 | 해결 근거 | FR-013 |
resolvedById | UUID | 아니오 | 처리 담당자 | FR-013, FR-024 |
resolvedAt | 날짜·시간 | 아니오 | 처리 시점 | FR-013 |
createdAt | 날짜·시간 | 예 | 오류 발견 시점 | FR-013 |
errorType에는 최소한 다음 값을 둡니다.
UNMAPPED_PRODUCTRULE_NOT_FOUNDRULE_CONFLICTDUPLICATE_TRANSACTIONUNCONFIRMED_DISCOUNTUNCONFIRMED_RETURNUNCONFIRMED_DISPOSALOUT_OF_PERIOD_TRANSACTIONRECONCILIATION_MISMATCH
BLOCKING 상태의 미해결 오류가 하나라도 있으면 정산서 생성 또는 정산 마감을 할 수 없습니다. (FR-013, FR-016, FR-017)
reconciliations — 대조
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 대조 식별값 | FR-014, FR-017 |
settlementWeekId | UUID | 예 | 대조 대상 정산 주차 | FR-014 |
cashierNetSalesAmount | 정수 | 예 | 계산대 판매 파일 순 판매액 | FR-014 |
manualAdjustmentAmount | 정수 | 예 | 근거가 연결된 수기 조정 합계 | FR-014 |
adjustedNetSalesAmount | 정수 | 예 | 조정된 순 판매액 | FR-014 |
farmerPayoutAmount | 정수 | 예 | 정정 제외 농가 지급액 합계 | FR-014 |
commissionAmount | 정수 | 예 | 수수료 합계 | FR-014 |
farmerBurdenAmount | 정수 | 예 | 농가 부담 반품·폐기 합계 | FR-014 |
storeCompensationAmount | 정수 | 예 | 매장 부담 보전 합계 | FR-014 |
currentSettlementAmount | 정수 | 예 | 현재 주차 정산 금액 | FR-014 |
differenceAmount | 정수 | 예 | 매출 대조 차이 | FR-014 |
calculatedAt | 날짜·시간 | 예 | 계산 시점 | FR-014 |
calculatedById | UUID | 아니오 | 수기 조정 포함 시 계산 실행 담당자 | FR-014, FR-024 |
대조 차이는 아래 방식으로 계산합니다.
조정된 순 판매액
= 계산대 판매 파일 순 판매액
± 확인된 수기 조정
현재 주차 정산 금액
= 농가 지급액 합계(정정 항목 제외)
+ 수수료 합계
+ 농가 부담 차감 합계
- 매장 부담 보전 합계
매출 대조 차이
= 조정된 순 판매액 - 현재 주차 정산 금액
differenceAmount = 0이고 미해결 필수 오류가 없을 때만 마감 가능 상태가 됩니다. (FR-014, FR-015, FR-017)
5.7 농가별 정산서와 마감
settlements — 정산
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 농가별 정산 식별값 | FR-012, FR-016 |
settlementWeekId | UUID | 예 | 소속 정산 주차 | FR-012 |
farmerId | UUID | 예 | 정산 대상 농가 | FR-012, FR-016 |
grossSalesAmount | 정수 | 예 | 정상 판매금액 합계 | FR-012, FR-016 |
discountAmount | 정수 | 예 | 할인금액 합계 | FR-012, FR-016 |
netSalesAmount | 정수 | 예 | 반품 등을 반영한 순 판매액 | FR-012, FR-016 |
returnAmount | 정수 | 예 | 반품 금액 합계 | FR-012, FR-016 |
commissionAmount | 정수 | 예 | 수수료 합계 | FR-012, FR-016 |
farmerReturnBurdenAmount | 정수 | 예 | 농가 부담 반품 금액 합계 | FR-012, FR-016 |
farmerDisposalBurdenAmount | 정수 | 예 | 농가 부담 폐기 금액 합계 | FR-012, FR-016 |
storeCompensationAmount | 정수 | 예 | 매장 부담 보전 금액 합계 | FR-012, FR-016 |
correctionAmount | 정수 | 예 | 이번 정산에 반영된 정정 항목 합계 | FR-012, FR-018 |
payoutAmount | 정수 | 예 | 최종 지급액 | FR-012, FR-016 |
calculationSnapshot | JSON | 예 | 거래별 계산 근거와 적용 규칙 요약 | FR-012, FR-017 |
calculatedAt | 날짜·시간 | 예 | 계산 시점 | FR-012 |
createdAt | 날짜·시간 | 예 | 생성 시점 | FR-016 |
updatedAt | 날짜·시간 | 예 | 마감 전 재계산 시점 | FR-016 |
농가 지급액은 다음 방식으로 계산합니다.
농가 지급액
= 해당 주차의 순 판매액
- 수수료
- 농가 부담 반품 금액
- 농가 부담 폐기 금액
+ 확정된 매장 부담 보전 금액
± 정정 항목
settlement_statements — 정산서
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 정산서 식별값 | FR-016~023 |
settlementId | UUID | 예 | 표시할 농가별 정산 | FR-016 |
statementNumber | 문자열 | 예 | 농가와 운영자가 확인할 정산서 번호 | FR-016, FR-021 |
status | 열거형 | 예 | GENERATED, CLOSED | FR-016, FR-017 |
expectedDepositDateSnapshot | 날짜 | 예 | 생성 시점 입금 예정일 복사본 | FR-016, FR-020 |
statementSnapshot | JSON | 예 | 정산서에 표시한 항목과 금액의 복사본 | FR-016, FR-017 |
generatedAt | 날짜·시간 | 예 | 정산서 생성 시점 | FR-016 |
generatedById | UUID | 예 | 생성 담당자 | FR-016, FR-024 |
closedAt | 날짜·시간 | 아니오 | 정산 마감 완료 시점 | FR-017 |
closedById | UUID | 아니오 | 마감 권한자 | FR-017, FR-024 |
statementSnapshot에는 농가가 조회할 다음 정보를 넣습니다.
- 이번 주 판매금액
- 할인금액
- 순 판매액
- 수수료
- 반품 금액과 농가 부담 금액
- 폐기 금액과 농가 부담 금액
- 정정 항목
- 최종 지급액
- 입금 예정일
- 품목별 세부 내역
마감 후에는 이 복사본을 바꾸지 않습니다. 농가 화면은 마감된 정산서의 복사본을 읽어 보여 줍니다. (FR-017, FR-020, FR-021, NFR-003)
5.8 정정 항목
correction_items — 정정 항목
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 정정 항목 식별값 | FR-018, FR-019 |
originalStatementId | UUID | 예 | 오류가 발견된 원 정산서 | FR-018 |
targetSettlementWeekId | UUID | 예 | 다음 정산 반영 대상 주차 | FR-018 |
farmerId | UUID | 예 | 정정 대상 농가 | FR-018 |
amount | 정수 | 예 | 증감액. 지급액 증가면 양수, 감소면 음수 | FR-018 |
reason | 문자열 | 예 | 정정 사유 | FR-018, FR-019 |
status | 열거형 | 예 | DRAFT, CONFIRMED, APPLIED, CANCELLED | FR-018 |
createdById | UUID | 예 | 작성 담당자 | FR-018, FR-024 |
confirmedById | UUID | 아니오 | 확정 담당자 | FR-018, FR-024 |
confirmedAt | 날짜·시간 | 아니오 | 확정 시점 | FR-018 |
appliedAt | 날짜·시간 | 아니오 | 다음 정산 계산 반영 시점 | FR-018 |
createdAt | 날짜·시간 | 예 | 작성 시점 | FR-018 |
정정 항목은 원 정산서의 금액을 바꾸지 않습니다. CONFIRMED 상태의 정정 항목만 다음 정산 주차의 농가 지급액에 반영합니다. (FR-018, FR-019, NFR-003)
5.9 농가 이의 제기와 증빙
disputes — 이의 제기
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 이의 제기 식별값 | FR-022, FR-023 |
settlementStatementId | UUID | 예 | 대상 정산서 | FR-022 |
farmerId | UUID | 예 | 이의를 제기한 농가 | FR-022 |
salesLedgerId | UUID | 아니오 | 특정 판매·할인·반품 거래 대상 | FR-022 |
disposalId | UUID | 아니오 | 특정 폐기 대상 | FR-022 |
content | 문자열 | 예 | 농가가 작성한 이의 내용 | FR-022 |
status | 열거형 | 예 | SUBMITTED, IN_REVIEW, RESOLVED, REJECTED | FR-022, FR-023 |
result | 문자열 | 아니오 | 운영자의 처리 결과 | FR-023 |
handledById | UUID | 아니오 | 처리 담당자 | FR-023 |
handledAt | 날짜·시간 | 아니오 | 처리일 | FR-023 |
farmerNotifiedAt | 날짜·시간 | 아니오 | 농가 안내 날짜 | FR-023 |
notificationMethod | 문자열 | 아니오 | 예: 전화, 대면, 정산서 확인 | FR-023 |
createdAt | 날짜·시간 | 예 | 제출 시점 | FR-022 |
updatedAt | 날짜·시간 | 예 | 상태 변경 시점 | FR-023 |
evidences — 증빙
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 증빙 식별값 | FR-011, FR-022 |
disputeId | UUID | 아니오 | 이의 제기 증빙인 경우 연결 | FR-022, FR-023 |
disposalId | UUID | 아니오 | 폐기 증빙인 경우 연결 | FR-011 |
storageKey | 문자열 | 예 | 사진 파일 저장 경로 또는 객체 저장소 키 | FR-011, FR-022 |
originalFileName | 문자열 | 예 | 원본 파일명 | FR-011, FR-022 |
mimeType | 문자열 | 예 | 허용된 이미지 형식인지 확인 | FR-022, NFR-006 |
fileSize | 정수 | 예 | 파일 크기 검사 | FR-022, NFR-006 |
uploadedById | UUID | 예 | 제출자 또는 등록 담당자 | FR-011, FR-022 |
createdAt | 날짜·시간 | 예 | 업로드 시점 | FR-011, FR-022 |
증빙 파일은 이미지 형식과 최대 크기를 제한하고, 로그인한 사용자 권한에 따라 제한된 주소로만 조회하게 합니다. (FR-022, NFR-004, NFR-006)
5.10 주요 작업 이력
activity_logs — 주요 작업 이력
| 필드 | 형식 | 필수 | 설명 | 관련 요구사항 |
|---|---|---|---|---|
id | UUID | 예 | 작업 이력 식별값 | FR-024 |
actorUserId | UUID | 아니오 | 작업한 로그인 계정. 시스템 작업은 비어 있을 수 있음 | FR-024 |
actionType | 열거형 | 예 | 작업 유형 | FR-024 |
targetType | 문자열 | 예 | 대상 데이터 종류 | FR-024 |
targetId | UUID | 아니오 | 대상 데이터 식별값 | FR-024 |
settlementWeekId | UUID | 아니오 | 관련 정산 주차 | FR-017~019, FR-024 |
beforeData | JSON | 아니오 | 변경 전 주요 값 | FR-002, FR-006, FR-024 |
afterData | JSON | 아니오 | 변경 후 주요 값 | FR-002, FR-006, FR-024 |
reason | 문자열 | 아니오 | 정정·상태 변경 사유 | FR-018, FR-023, FR-024 |
createdAt | 날짜·시간 | 예 | 작업 시점 | FR-024 |
기록 대상 작업 예시는 다음과 같습니다.
- 로그인 성공·실패
- 계정 생성, 권한 변경, 중지
- 농가·품목·상품코드 등록과 사용 중지
- 정산 규칙 생성과 종료일 변경
- 계산대 판매 파일 업로드와 처리 결과
- 상품코드 직접 연결과 변경
- 폐기 등록
- 오류 해결
- 정산서 생성
- 정산 마감
- 정정 항목 생성·확정·반영
- 이의 제기 처리와 농가 안내 기록
마감 후 자료의 직접 변경을 허용하지 않고, 필요한 후속 처리는 정정 항목과 작업 이력으로 남깁니다. (FR-017~019, FR-024, NFR-003, NFR-007)
6. 데이터 생명주기
그림을 그리는 중…
정산 주차는 판매 파일을 처리하고 오류와 대조 차이를 모두 해결한 뒤에만 마감할 수 있습니다. 마감 이후에는 원 정산서와 계산 근거를 유지하며, 차액은 다음 정산 주차에 정정 항목으로 반영합니다. (FR-007~019)
7. 핵심 무결성 규칙
| 규칙 | 구현 기준 | 관련 요구사항 |
|---|---|---|
| 정산 주차 기간 중복 방지 | 같은 매장에서 periodStartDate~periodEndDate가 겹치는 정산 주차 생성 차단 | FR-004 |
| 상품코드 중복 연결 방지 | 활성 상태의 동일 productCode는 하나의 품목에만 연결 | FR-005, FR-008 |
| 정산 규칙 기간 중복 방지 | 같은 적용 대상 조합의 유효 기간이 겹치면 저장 차단 | FR-006 |
| 규칙 없는 거래 계산 방지 | 적용 규칙이 없거나 충돌하면 settlement_errors 생성 후 계산·마감 차단 | FR-006, FR-012, FR-013 |
| 중복 거래 반영 방지 | externalTransactionId와 원본 파일 정보를 검사해 중복 의심 거래 제외 | FR-009 |
| 마감 전 오류 해결 의무 | 미해결 BLOCKING 오류 또는 대조 차이가 있으면 마감 차단 | FR-013, FR-014, FR-017 |
| 마감 후 불변성 | CLOSED 정산 주차의 판매 원장·정산·정산서·규칙 적용 결과 수정 차단 | FR-017, NFR-003 |
| 정정의 다음 주차 반영 | 정정 항목은 원 정산서를 참조하고, 다음 정산 주차에만 반영 | FR-018, FR-019 |
| 농가 자료 격리 | 농가 로그인 계정은 연결된 farmerId의 정산서·이의 제기만 조회 | FR-003, FR-020~022, NFR-004 |
8. 필요한 인덱스
| 표 | 인덱스 또는 제약 | 사용 목적 | 관련 요구사항 |
|---|---|---|---|
users | loginId 고유 인덱스 | 로그인 계정 조회 | FR-001, FR-003 |
users | farmerId 고유 인덱스, 농가 계정에만 적용 | 하나의 계정을 여러 농가에 연결하지 않음 | FR-003 |
products | (farmerId, isActive) | 농가별 사용 품목 조회 | FR-005, FR-008 |
product_code_mappings | 활성 productCode 부분 고유 인덱스 | 자동 상품 연결과 중복 방지 | FR-005, FR-008 |
settlement_rules | (farmerId, productId, productGroupId, effectiveFrom, effectiveTo) | 판매일 기준 규칙 탐색과 중복 검사 | FR-006, FR-012 |
settlement_weeks | (periodStartDate, periodEndDate) | 기간 중복 검사 | FR-004 |
cashier_sales_files | (settlementWeekId, fileHash) 고유 인덱스 | 같은 파일 중복 업로드 차단 | FR-007 |
cashier_sales_file_rows | (cashierSalesFileId, rowNumber) 고유 인덱스 | 원본 행 식별 | FR-007 |
sales_ledgers | (settlementWeekId, mappingStatus, settlementStatus) | 미연결·오류 거래 목록 조회 | FR-008, FR-013 |
sales_ledgers | externalTransactionId 인덱스 | 중복 거래 검사 | FR-009 |
settlements | (settlementWeekId, farmerId) 고유 인덱스 | 주차별 농가 정산 하나만 생성 | FR-012, FR-016 |
settlement_statements | statementNumber 고유 인덱스 | 정산서 조회·내려받기 | FR-016, FR-021 |
correction_items | (targetSettlementWeekId, farmerId, status) | 다음 주차 정정 반영 계산 | FR-018 |
disputes | (farmerId, status, createdAt) | 농가 본인 이의 제기 조회 | FR-022, FR-023 |
settlement_errors | (settlementWeekId, status, severity) | 마감 차단 오류 수 계산 | FR-013, FR-015, FR-017 |
activity_logs | (targetType, targetId, createdAt) | 대상별 변경 이력 조회 | FR-019, FR-024 |
9. Prisma 모델 초안
아래 초안은 PostgreSQL 기준입니다. 정산 규칙의 기간 중복 검증, 농가 계정의 조건부 고유 제약, 마감 상태의 수정 차단은 Prisma 모델만으로 충분하지 않으므로 데이터베이스 제약과 서버 로직에서 함께 처리합니다.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
STORE_OWNER
SETTLEMENT_MANAGER
CLOSING_MANAGER
FARMER
}
enum UserStatus {
ACTIVE
SUSPENDED
}
enum SettlementWeekStatus {
PREPARING
PROCESSING
HAS_ERRORS
READY_TO_CLOSE
STATEMENTS_GENERATED
CLOSED
CORRECTION_PENDING
}
enum FileFormat {
CSV
XLSX
}
enum SalesFileStatus {
UPLOADED
VALIDATING
PARTIALLY_PROCESSED
PROCESSED
FAILED
}
enum FileRowValidationStatus {
VALID
INVALID
PENDING_REVIEW
}
enum ProductCodeMappingSource {
MANUAL
AUTO_CONFIRMED
}
enum DiscountCommissionBasis {
BEFORE_DISCOUNT
AFTER_DISCOUNT
}
enum BurdenParty {
FARMER
STORE
NONE
}
enum DisposalCalculationMethod {
MANUAL_AMOUNT
ORIGINAL_PRICE
ACTUAL_COST
}
enum TransactionType {
SALE
DISCOUNT_SALE
RETURN
MANUAL_ADJUSTMENT
}
enum MappingStatus {
AUTO_MAPPED
MANUALLY_MAPPED
UNMAPPED
CONFLICTED
}
enum LedgerSettlementStatus {
PENDING
CALCULATED
ERROR
EXCLUDED
}
enum DisposalType {
EXPIRATION
UNCLAIMED_STOCK
OTHER
}
enum SettlementErrorType {
UNMAPPED_PRODUCT
RULE_NOT_FOUND
RULE_CONFLICT
DUPLICATE_TRANSACTION
UNCONFIRMED_DISCOUNT
UNCONFIRMED_RETURN
UNCONFIRMED_DISPOSAL
OUT_OF_PERIOD_TRANSACTION
RECONCILIATION_MISMATCH
}
enum ErrorSeverity {
BLOCKING
WARNING
}
enum ErrorStatus {
OPEN
RESOLVED
}
enum StatementStatus {
GENERATED
CLOSED
}
enum CorrectionStatus {
DRAFT
CONFIRMED
APPLIED
CANCELLED
}
enum DisputeStatus {
SUBMITTED
IN_REVIEW
RESOLVED
REJECTED
}
model User {
id String @id @default(uuid()) @db.Uuid
loginId String @unique
passwordHash String
role UserRole
farmerId String? @unique @db.Uuid
status UserStatus @default(ACTIVE)
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
farmer Farmer? @relation("FarmerUser", fields: [farmerId], references: [id])
createdSettlementWeeks SettlementWeek[] @relation("SettlementWeekCreator")
closedSettlementWeeks SettlementWeek[] @relation("SettlementWeekCloser")
uploadedSalesFiles CashierSalesFile[] @relation("SalesFileUploader")
createdMappings ProductCodeMapping[]
createdRules SettlementRule[]
handledDisposals Disposal[]
resolvedErrors SettlementError[]
generatedStatements SettlementStatement[] @relation("StatementGenerator")
closedStatements SettlementStatement[] @relation("StatementCloser")
createdCorrections CorrectionItem[] @relation("CorrectionCreator")
confirmedCorrections CorrectionItem[] @relation("CorrectionConfirmer")
handledDisputes Dispute[] @relation("DisputeHandler")
uploadedEvidences Evidence[]
activityLogs ActivityLog[]
@@index([role, status])
}
model Farmer {
id String @id @default(uuid()) @db.Uuid
name String
isActive Boolean @default(true)
deactivatedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation("FarmerUser")
products Product[]
rules SettlementRule[]
ledgers SalesLedger[]
disposals Disposal[]
settlements Settlement[]
corrections CorrectionItem[]
disputes Dispute[]
@@index([isActive, name])
}
model ProductGroup {
id String @id @default(uuid()) @db.Uuid
name String @unique
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
products Product[]
rules SettlementRule[]
}
model Product {
id String @id @default(uuid()) @db.Uuid
farmerId String @db.Uuid
productGroupId String? @db.Uuid
name String
specification String?
isActive Boolean @default(true)
deactivatedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
farmer Farmer @relation(fields: [farmerId], references: [id])
productGroup ProductGroup? @relation(fields: [productGroupId], references: [id])
codeMappings ProductCodeMapping[]
rules SettlementRule[]
ledgers SalesLedger[]
disposals Disposal[]
@@index([farmerId, isActive])
@@index([productGroupId, isActive])
}
model ProductCodeMapping {
id String @id @default(uuid()) @db.Uuid
productCode String
productId String @db.Uuid
mappingSource ProductCodeMappingSource
isActive Boolean @default(true)
createdById String @db.Uuid
createdAt DateTime @default(now())
endedAt DateTime?
product Product @relation(fields: [productId], references: [id])
createdBy User @relation(fields: [createdById], references: [id])
@@index([productCode, isActive])
@@index([productId])
}
model SettlementRule {
id String @id @default(uuid()) @db.Uuid
name String
farmerId String? @db.Uuid
productId String? @db.Uuid
productGroupId String? @db.Uuid
commissionRate Decimal @db.Decimal(7, 4)
discountCommissionBasis DiscountCommissionBasis
returnBurdenParty BurdenParty
disposalBurdenParty BurdenParty
disposalCalculationMethod DisposalCalculationMethod
effectiveFrom DateTime @db.Date
effectiveTo DateTime? @db.Date
isConfirmed Boolean @default(false)
createdById String @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
farmer Farmer? @relation(fields: [farmerId], references: [id])
product Product? @relation(fields: [productId], references: [id])
productGroup ProductGroup? @relation(fields: [productGroupId], references: [id])
createdBy User @relation(fields: [createdById], references: [id])
ledgers SalesLedger[]
disposals Disposal[]
@@index([farmerId, productId, productGroupId, effectiveFrom, effectiveTo])
@@index([effectiveFrom, effectiveTo, isConfirmed])
}
model SettlementWeek {
id String @id @default(uuid()) @db.Uuid
periodStartDate DateTime @db.Date
periodEndDate DateTime @db.Date
closingDate DateTime @db.Date
expectedDepositDate DateTime @db.Date
status SettlementWeekStatus @default(PREPARING)
closedAt DateTime?
closedById String? @db.Uuid
createdById String @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User @relation("SettlementWeekCreator", fields: [createdById], references: [id])
closedBy User? @relation("SettlementWeekCloser", fields: [closedById], references: [id])
salesFiles CashierSalesFile[]
ledgers SalesLedger[]
disposals Disposal[]
errors SettlementError[]
settlements Settlement[]
corrections CorrectionItem[]
reconciliations Reconciliation[]
activityLogs ActivityLog[]
@@index([periodStartDate, periodEndDate])
@@index([status, closingDate])
}
model CashierSalesFile {
id String @id @default(uuid()) @db.Uuid
settlementWeekId String @db.Uuid
originalFileName String
storageKey String
fileHash String
fileFormat FileFormat
status SalesFileStatus @default(UPLOADED)
totalRowCount Int @default(0)
validRowCount Int @default(0)
errorRowCount Int @default(0)
uploadedById String @db.Uuid
uploadedAt DateTime @default(now())
processedAt DateTime?
settlementWeek SettlementWeek @relation(fields: [settlementWeekId], references: [id])
uploadedBy User @relation("SalesFileUploader", fields: [uploadedById], references: [id])
rows CashierSalesFileRow[]
ledgers SalesLedger[]
@@unique([settlementWeekId, fileHash])
@@index([settlementWeekId, status])
}
model CashierSalesFileRow {
id String @id @default(uuid()) @db.Uuid
cashierSalesFileId String @db.Uuid
rowNumber Int
rawData Json
transactionExternalId String?
saleDate DateTime?
productCode String?
productNameRaw String?
validationStatus FileRowValidationStatus @default(PENDING_REVIEW)
validationMessage String?
createdAt DateTime @default(now())
cashierSalesFile CashierSalesFile @relation(fields: [cashierSalesFileId], references: [id])
ledgers SalesLedger[]
@@unique([cashierSalesFileId, rowNumber])
@@index([transactionExternalId])
@@index([productCode, validationStatus])
}
model SalesLedger {
id String @id @default(uuid()) @db.Uuid
settlementWeekId String @db.Uuid
cashierSalesFileId String? @db.Uuid
cashierSalesFileRowId String? @db.Uuid
farmerId String? @db.Uuid
productId String? @db.Uuid
transactionType TransactionType
transactionDate DateTime
externalTransactionId String?
quantity Decimal @db.Decimal(12, 3)
regularSaleAmount Int @default(0)
actualSaleAmount Int @default(0)
discountAmount Int @default(0)
returnAmount Int @default(0)
netSalesAmount Int @default(0)
mappingStatus MappingStatus @default(UNMAPPED)
settlementStatus LedgerSettlementStatus @default(PENDING)
appliedRuleId String? @db.Uuid
appliedRuleSnapshot Json?
commissionBaseAmount Int?
commissionAmount Int?
farmerReturnBurdenAmount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
settlementWeek SettlementWeek @relation(fields: [settlementWeekId], references: [id])
cashierSalesFile CashierSalesFile? @relation(fields: [cashierSalesFileId], references: [id])
cashierSalesFileRow CashierSalesFileRow? @relation(fields: [cashierSalesFileRowId], references: [id])
farmer Farmer? @relation(fields: [farmerId], references: [id])
product Product? @relation(fields: [productId], references: [id])
appliedRule SettlementRule? @relation(fields: [appliedRuleId], references: [id])
errors SettlementError[]
disputes Dispute[]
@@index([settlementWeekId, mappingStatus, settlementStatus])
@@index([settlementWeekId, farmerId, productId])
@@index([externalTransactionId])
}
model Disposal {
id String @id @default(uuid()) @db.Uuid
settlementWeekId String @db.Uuid
farmerId String @db.Uuid
productId String @db.Uuid
disposalType DisposalType
reasonDetail String?
quantity Decimal @db.Decimal(12, 3)
burdenParty BurdenParty
settlementRuleId String? @db.Uuid
settlementRuleSnapshot Json?
settlementImpactAmount Int
notifiedFarmerAt DateTime?
notificationMethod String?
handledById String @db.Uuid
handledAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
settlementWeek SettlementWeek @relation(fields: [settlementWeekId], references: [id])
farmer Farmer @relation(fields: [farmerId], references: [id])
product Product @relation(fields: [productId], references: [id])
settlementRule SettlementRule? @relation(fields: [settlementRuleId], references: [id])
handledBy User @relation(fields: [handledById], references: [id])
evidences Evidence[]
errors SettlementError[]
disputes Dispute[]
@@index([settlementWeekId, farmerId, productId])
}
model SettlementError {
id String @id @default(uuid()) @db.Uuid
settlementWeekId String @db.Uuid
salesLedgerId String? @db.Uuid
disposalId String? @db.Uuid
errorType SettlementErrorType
severity ErrorSeverity
status ErrorStatus @default(OPEN)
message String
resolutionNote String?
resolvedById String? @db.Uuid
resolvedAt DateTime?
createdAt DateTime @default(now())
settlementWeek SettlementWeek @relation(fields: [settlementWeekId], references: [id])
salesLedger SalesLedger? @relation(fields: [salesLedgerId], references: [id])
disposal Disposal? @relation(fields: [disposalId], references: [id])
resolvedBy User? @relation(fields: [resolvedById], references: [id])
@@index([settlementWeekId, status, severity])
}
model Reconciliation {
id String @id @default(uuid()) @db.Uuid
settlementWeekId String @db.Uuid
cashierNetSalesAmount Int
manualAdjustmentAmount Int @default(0)
adjustedNetSalesAmount Int
farmerPayoutAmount Int
commissionAmount Int
farmerBurdenAmount Int
storeCompensationAmount Int
currentSettlementAmount Int
differenceAmount Int
calculatedAt DateTime @default(now())
calculatedById String? @db.Uuid
settlementWeek SettlementWeek @relation(fields: [settlementWeekId], references: [id])
@@index([settlementWeekId, calculatedAt])
}
model Settlement {
id String @id @default(uuid()) @db.Uuid
settlementWeekId String @db.Uuid
farmerId String @db.Uuid
grossSalesAmount Int @default(0)
discountAmount Int @default(0)
netSalesAmount Int @default(0)
returnAmount Int @default(0)
commissionAmount Int @default(0)
farmerReturnBurdenAmount Int @default(0)
farmerDisposalBurdenAmount Int @default(0)
storeCompensationAmount Int @default(0)
correctionAmount Int @default(0)
payoutAmount Int @default(0)
calculationSnapshot Json
calculatedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
settlementWeek SettlementWeek @relation(fields: [settlementWeekId], references: [id])
farmer Farmer @relation(fields: [farmerId], references: [id])
statement SettlementStatement?
@@unique([settlementWeekId, farmerId])
@@index([farmerId, createdAt])
}
model SettlementStatement {
id String @id @default(uuid()) @db.Uuid
settlementId String @unique @db.Uuid
statementNumber String @unique
status StatementStatus @default(GENERATED)
expectedDepositDateSnapshot DateTime @db.Date
statementSnapshot Json
generatedAt DateTime @default(now())
generatedById String @db.Uuid
closedAt DateTime?
closedById String? @db.Uuid
settlement Settlement @relation(fields: [settlementId], references: [id])
generatedBy User @relation("StatementGenerator", fields: [generatedById], references: [id])
closedBy User? @relation("StatementCloser", fields: [closedById], references: [id])
corrections CorrectionItem[]
disputes Dispute[]
@@index([status, expectedDepositDateSnapshot])
}
model CorrectionItem {
id String @id @default(uuid()) @db.Uuid
originalStatementId String @db.Uuid
targetSettlementWeekId String @db.Uuid
farmerId String @db.Uuid
amount Int
reason String
status CorrectionStatus @default(DRAFT)
createdById String @db.Uuid
confirmedById String? @db.Uuid
confirmedAt DateTime?
appliedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
originalStatement SettlementStatement @relation(fields: [originalStatementId], references: [id])
targetSettlementWeek SettlementWeek @relation(fields: [targetSettlementWeekId], references: [id])
farmer Farmer @relation(fields: [farmerId], references: [id])
createdBy User @relation("CorrectionCreator", fields: [createdById], references: [id])
confirmedBy User? @relation("CorrectionConfirmer", fields: [confirmedById], references: [id])
@@index([targetSettlementWeekId, farmerId, status])
@@index([originalStatementId])
}
model Dispute {
id String @id @default(uuid()) @db.Uuid
settlementStatementId String @db.Uuid
farmerId String @db.Uuid
salesLedgerId String? @db.Uuid
disposalId String? @db.Uuid
content String
status DisputeStatus @default(SUBMITTED)
result String?
handledById String? @db.Uuid
handledAt DateTime?
farmerNotifiedAt DateTime?
notificationMethod String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
settlementStatement SettlementStatement @relation(fields: [settlementStatementId], references: [id])
farmer Farmer @relation(fields: [farmerId], references: [id])
salesLedger SalesLedger? @relation(fields: [salesLedgerId], references: [id])
disposal Disposal? @relation(fields: [disposalId], references: [id])
handledBy User? @relation("DisputeHandler", fields: [handledById], references: [id])
evidences Evidence[]
@@index([farmerId, status, createdAt])
@@index([settlementStatementId, status])
}
model Evidence {
id String @id @default(uuid()) @db.Uuid
disputeId String? @db.Uuid
disposalId String? @db.Uuid
storageKey String
originalFileName String
mimeType String
fileSize Int
uploadedById String @db.Uuid
createdAt DateTime @default(now())
dispute Dispute? @relation(fields: [disputeId], references: [id])
disposal Disposal? @relation(fields: [disposalId], references: [id])
uploadedBy User @relation(fields: [uploadedById], references: [id])
@@index([disputeId])
@@index([disposalId])
}
model ActivityLog {
id String @id @default(uuid()) @db.Uuid
actorUserId String? @db.Uuid
actionType String
targetType String
targetId String?
settlementWeekId String? @db.Uuid
beforeData Json?
afterData Json?
reason String?
createdAt DateTime @default(now())
actorUser User? @relation(fields: [actorUserId], references: [id])
settlementWeek SettlementWeek? @relation(fields: [settlementWeekId], references: [id])
@@index([targetType, targetId, createdAt])
@@index([settlementWeekId, createdAt])
@@index([actorUserId, createdAt])
}
10. 구현 시 확인할 항목
| 확인 항목 | 데이터 구조 영향 | 관련 요구사항 |
|---|---|---|
| 계산대 판매 파일의 실제 열 구성 | cashier_sales_file_rows.rawData에서 추출할 거래일, 거래 고유값, 상품코드, 판매금액, 할인금액, 반품금액 열을 확정해야 함 | FR-007, FR-009, FR-010 |
| 거래 고유값이 없는 파일의 중복 판별 기준 | externalTransactionId가 없을 때 거래일·상품코드·금액·수량 등의 조합을 중복 검사 기준으로 정해야 함 | FR-009 |
| 수수료 원 단위 반올림 기준 | 절사, 반올림, 올림 중 매장 전체 공통 방식을 정하고 계산 로직에 고정해야 함 | FR-012, NFR-001 |
| 음수 지급액 처리 | 지급액이 음수일 때 마감 허용 여부와 다음 정산 이월 방식이 필요함 | FR-012 |
| 농가 최초 로그인과 비밀번호 복구 | 임시 비밀번호, 관리자 발급, 본인 확인 방식 중 하나를 정해야 함 | FR-001, FR-003, NFR-005 |
| 폐기 통지 기록의 마감 필수 여부 | 현재는 농가 부담 폐기 시 경고로 설계했으며, 마감 차단 조건으로 할지는 확정 필요 | FR-011, FR-013 |
| 증빙 보관 기간과 삭제 기준 | 증빙 사진의 저장 기간, 접근 범위, 백업·삭제 절차를 정해야 함 | FR-011, FR-022, NFR-004, NFR-011 |