화면과 서버를 이을 때 읽는 문서

기능 연결 방식

어떤 요청이 오가고 무엇이 돌아오는지, 알림톡 발송 결과를 예약 확인 건에 어떻게 되돌려 적는지 적었습니다.

33,792자 · 시스템이 만든 그대로입니다

온헤어 CRM 기능 연결 방식(API) 문서

공통 규칙

  • 기준 시간대: Asia/Seoul
  • 웹 클라이언트: Next.js
  • 인증 방식: 로그인 후 발급된 세션 쿠키 사용
  • 모든 데이터는 로그인한 직원의 가게 범위에서만 조회·수정한다. (FR-001, FR-005, NFR-002)
  • 원장님 전용 기능은 서버에서 역할을 다시 확인한다. 화면 숨김만으로 권한을 처리하지 않는다. (FR-002, FR-005, NFR-003)
  • 날짜·시간 값은 ISO 8601 형식으로 전달한다. 예: 2026-09-16T18:00:00+09:00
  • 목록 조회 기본 응답 형식:
{
  "items": [],
  "page": 1,
  "pageSize": 20,
  "total": 0
}
  • 오류 응답 기본 형식:
{
  "error": {
    "code": "CUSTOMER_NOT_FOUND",
    "message": "고객을 찾을 수 없습니다.",
    "field": "customerId"
  }
}

1. 로그인과 가게 확인

POST /api/auth/login

  • 목적: 활성 상태인 원장님 또는 디자이너를 로그인하고, 소속 가게와 역할 범위의 세션을 만든다. (FR-001, UIR-001)
  • 인증: 필요 없음
  • 요청값:
{
  "loginId": "designer@example.com",
  "password": "비밀번호"
}
  • 성공 응답:
{
  "user": {
    "id": "staff_123",
    "name": "김디자이너",
    "role": "designer",
    "storeId": "store_001",
    "storeName": "온헤어"
  },
  "redirectTo": "/reservations/today-tomorrow"
}
  • 사용자에게 표시할 오류 상황:
    • 로그인 정보가 맞지 않음: “로그인 정보를 다시 확인해 주세요.”
    • 계정이 차단됨: “사용이 중지된 계정입니다. 원장님에게 문의해 주세요.”
    • 소속 가게가 없거나 가게가 비활성 상태임: “현재 가게에 접근할 수 없습니다.”

POST /api/auth/logout

  • 목적: 현재 로그인 세션을 종료한다. (FR-001)
  • 인증: 필요
  • 요청값: 없음
  • 성공 응답:
{
  "success": true
}
  • 사용자에게 표시할 오류 상황:
    • 세션이 이미 만료됨: 로그인 화면으로 이동한다.

GET /api/auth/me

  • 목적: 로그인 상태, 직원 역할, 가게 정보를 확인하고 첫 화면의 접근 범위를 결정한다. (FR-001, FR-005)
  • 인증: 필요
  • 요청값: 없음
  • 성공 응답:
{
  "user": {
    "id": "staff_123",
    "name": "김디자이너",
    "role": "designer",
    "isActive": true
  },
  "store": {
    "id": "store_001",
    "name": "온헤어",
    "timezone": "Asia/Seoul"
  },
  "permissions": {
    "canManageStaff": false,
    "canImportData": false,
    "canViewAllCustomers": false,
    "canViewActivityLogs": false
  }
}
  • 사용자에게 표시할 오류 상황:
    • 세션 만료: “로그인이 만료되었습니다. 다시 로그인해 주세요.”
    • 계정 차단: “사용이 중지된 계정입니다.”

2. 직원·권한 설정

GET /api/staff

  • 목적: 가게 직원 목록과 역할, 계정 상태, 마지막 권한 변경일을 조회한다. (FR-002, UIR-009)
  • 인증: 필요, 원장님만 가능
  • 요청값:
    • status: 선택. active, blocked, all
  • 성공 응답:
{
  "items": [
    {
      "id": "staff_123",
      "name": "김디자이너",
      "role": "designer",
      "loginMethod": "email",
      "isActive": true,
      "assignedCustomerCount": 128,
      "lastPermissionChangedAt": "2026-09-10T10:20:00+09:00"
    }
  ],
  "page": 1,
  "pageSize": 20,
  "total": 1
}
  • 사용자에게 표시할 오류 상황:
    • 디자이너가 접근함: “직원·권한 설정은 원장님만 사용할 수 있습니다.”

POST /api/staff

  • 목적: 새 직원 계정을 등록하고 원장님 또는 디자이너 역할을 지정한다. (FR-002, UIR-009)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "name": "이디자이너",
  "loginId": "lee@example.com",
  "loginMethod": "email",
  "role": "designer",
  "isActive": true
}
  • 성공 응답:
{
  "id": "staff_456",
  "name": "이디자이너",
  "role": "designer",
  "isActive": true,
  "createdAt": "2026-09-16T11:00:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 이미 사용 중인 로그인 정보: “이미 등록된 로그인 정보입니다.”
    • 이름 또는 로그인 수단 미입력: “직원 이름과 로그인 정보를 입력해 주세요.”
    • 다른 가게 직원 연결 시도: “다른 가게 직원은 등록할 수 없습니다.”

PATCH /api/staff/{staffId}

  • 목적: 직원 이름, 역할, 계정 활성 상태를 변경하고 권한 변경 이력을 남긴다. (FR-002, FR-028, UIR-009)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "name": "이디자이너",
  "role": "designer",
  "isActive": false,
  "changeReason": "퇴사 처리"
}
  • 성공 응답:
{
  "id": "staff_456",
  "name": "이디자이너",
  "role": "designer",
  "isActive": false,
  "lastPermissionChangedAt": "2026-09-16T11:30:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 마지막 원장님 계정을 차단하거나 디자이너로 변경하려 함: “마지막 원장님 계정은 차단하거나 역할을 변경할 수 없습니다.”
    • 존재하지 않는 직원: “직원을 찾을 수 없습니다.”
    • 현재 로그인한 원장님이 본인 계정을 차단함: “현재 사용 중인 계정은 직접 차단할 수 없습니다.”

3. 고객 등록, 검색, 상세 조회

GET /api/customers

  • 목적: 고객 목록에서 이름, 휴대전화 번호, 담당 디자이너로 고객을 검색한다. 디자이너는 자기 담당 고객만 조회한다. (FR-003, FR-005, UIR-003)
  • 인증: 필요
  • 요청값:
    • query: 선택. 이름 또는 휴대전화 번호
    • designerId: 선택. 원장님만 사용 가능
    • stage: 선택. 고객 단계
    • page, pageSize
  • 성공 응답:
{
  "items": [
    {
      "id": "customer_001",
      "name": "김고객",
      "displayPhone": "010-1234-5678",
      "assignedDesigner": {
        "id": "staff_123",
        "name": "김디자이너"
      },
      "stage": "repeat_customer",
      "lastTreatmentAt": "2026-08-10T14:00:00+09:00",
      "marketingConsentStatus": "agreed"
    }
  ],
  "page": 1,
  "pageSize": 20,
  "total": 1
}
  • 사용자에게 표시할 오류 상황:
    • 검색어가 너무 짧음: “이름 또는 휴대전화 번호를 2자 이상 입력해 주세요.”
    • 권한 없는 담당 고객 조회: 결과를 반환하지 않고 “조회 권한이 없습니다.”를 표시한다.

POST /api/customers/phone-check

  • 목적: 고객 등록 전 휴대전화 번호를 정규화하고, 같은 가게의 중복 고객 여부를 확인한다. (FR-003, FR-004)
  • 인증: 필요
  • 요청값:
{
  "rawPhone": "+82 10-1234-5678"
}
  • 성공 응답 — 등록 가능한 번호:
{
  "rawPhone": "+82 10-1234-5678",
  "normalizedPhone": "01012345678",
  "displayPhone": "010-1234-5678",
  "validationStatus": "valid",
  "duplicateCandidates": []
}
  • 성공 응답 — 기존 고객 존재:
{
  "rawPhone": "010-1234-5678",
  "normalizedPhone": "01012345678",
  "displayPhone": "010-1234-5678",
  "validationStatus": "valid",
  "duplicateCandidates": [
    {
      "id": "customer_001",
      "name": "김고객",
      "assignedDesignerName": "김디자이너",
      "accessAllowed": true
    }
  ]
}
  • 사용자에게 표시할 오류 상황:
    • 일반 전화번호 또는 자릿수 부족: “휴대전화 번호를 확인해 주세요. 자동 병합과 알림톡 발송은 할 수 없습니다.”
    • 011, 016, 017, 018, 019 번호: 기존 번호 그대로 검토 대상으로 처리한다.
    • 번호에 메모가 함께 입력됨: “전화번호 외 내용이 포함되어 있습니다. 번호를 따로 입력해 주세요.”

POST /api/customers

  • 목적: 새 고객을 등록하고 개인정보 및 마케팅 수신동의 상태를 저장한다. (FR-003, FR-004, FR-026)
  • 인증: 필요
  • 요청값:
{
  "name": "김고객",
  "rawPhone": "010-1234-5678",
  "assignedDesignerId": "staff_123",
  "privacyConsent": {
    "status": "agreed",
    "consentedAt": "2026-09-16T11:20:00+09:00",
    "channel": "매장"
  },
  "marketingConsent": {
    "status": "declined",
    "consentedAt": null,
    "channel": null
  }
}
  • 성공 응답:
{
  "id": "customer_001",
  "name": "김고객",
  "normalizedPhone": "01012345678",
  "displayPhone": "010-1234-5678",
  "validationStatus": "valid",
  "stage": "inquiry",
  "recordStatus": "active"
}
  • 사용자에게 표시할 오류 상황:
    • 같은 정규화 번호 고객 존재: “같은 휴대전화 번호의 고객이 이미 있습니다. 기존 고객을 확인해 주세요.”
    • 담당 디자이너가 현재 가게에 없음: “선택한 담당 디자이너를 사용할 수 없습니다.”
    • 개인정보 동의 상태 미입력: “개인정보 동의 상태를 선택해 주세요.”

GET /api/customers/{customerId}

  • 목적: 고객 상세에서 고객 정보, 동의 상태, 시술 이력, 다음 예약, 예약 확인 건을 한 번에 조회한다. (FR-005, FR-007, UIR-004)
  • 인증: 필요
  • 요청값: 없음
  • 성공 응답:
{
  "id": "customer_001",
  "name": "김고객",
  "displayPhone": "010-1234-5678",
  "assignedDesigner": {
    "id": "staff_123",
    "name": "김디자이너"
  },
  "stage": "next_reservation_booked",
  "privacyConsent": {
    "status": "agreed",
    "consentedAt": "2026-03-01T12:00:00+09:00",
    "channel": "매장"
  },
  "marketingConsent": {
    "status": "agreed",
    "consentedAt": "2026-03-01T12:00:00+09:00",
    "channel": "카카오톡 채널"
  },
  "photoConsent": {
    "status": "agreed",
    "retentionUntil": "2027-03-01"
  },
  "lastTreatmentAt": "2026-09-10T14:00:00+09:00",
  "treatmentRecords": [],
  "upcomingReservations": [],
  "recentReservationConfirmations": []
}
  • 사용자에게 표시할 오류 상황:
    • 병합된 고객 주소 접근: “통합된 고객입니다. 대표 고객 정보로 이동합니다.”
    • 다른 담당 고객 접근: FR-006에서 확정한 열람 정책에 따라 필요한 범위만 표시한다.
    • 삭제 또는 알아볼 수 없게 처리된 고객: “삭제 처리된 고객입니다. 허용된 최소 이력만 볼 수 있습니다.”

PATCH /api/customers/{customerId}

  • 목적: 고객 이름, 담당 디자이너, 동의 상태, 촬영·보관 동의 및 보유기간을 수정한다. (FR-003, FR-026)
  • 인증: 필요
  • 요청값:
{
  "name": "김고객",
  "assignedDesignerId": "staff_456",
  "privacyConsent": {
    "status": "agreed",
    "consentedAt": "2026-09-16T12:00:00+09:00",
    "channel": "매장"
  },
  "marketingConsent": {
    "status": "agreed",
    "consentedAt": "2026-09-16T12:00:00+09:00",
    "channel": "카카오톡 채널"
  },
  "photoConsent": {
    "status": "agreed",
    "consentedAt": "2026-09-16T12:00:00+09:00",
    "channel": "매장",
    "retentionUntil": "2027-09-16"
  }
}
  • 성공 응답:
{
  "id": "customer_001",
  "updatedAt": "2026-09-16T12:00:00+09:00",
  "stage": "next_reservation_booked"
}
  • 사용자에게 표시할 오류 상황:
    • 디자이너가 권한 없는 고객 수정: “이 고객을 수정할 권한이 없습니다.”
    • 사진 보유기간 없이 사진 동의 저장 시도: “시술 사진 보유기간을 함께 입력해 주세요.”
    • 이미 삭제 처리된 고객 수정: “삭제 처리된 고객은 수정할 수 없습니다.”

4. 대체 시술 열람

POST /api/customers/{customerId}/temporary-access-requests

  • 목적: 다른 담당 고객의 시술 이력, 염색약 번호, 배합을 확인해야 할 때 임시 열람을 요청한다. (FR-006)
  • 인증: 필요, 디자이너 또는 원장님
  • 요청값:
{
  "reason": "담당 디자이너 휴무로 염색약 번호 확인 필요",
  "requestedUntil": "2026-09-16T20:00:00+09:00"
}
  • 성공 응답:
{
  "id": "access_request_001",
  "status": "pending",
  "customerId": "customer_001",
  "requestedUntil": "2026-09-16T20:00:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 자기 담당 고객: “내 담당 고객은 별도 요청 없이 확인할 수 있습니다.”
    • 고객이 다른 가게 소속: “다른 가게 고객에게는 접근할 수 없습니다.”

PATCH /api/temporary-access-requests/{requestId}

  • 목적: 원장님이 임시 열람 요청을 승인하거나 거절한다. 승인 시 열람 기간과 승인자를 기록한다. (FR-006, FR-028)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "status": "approved",
  "approvedUntil": "2026-09-16T20:00:00+09:00"
}
  • 성공 응답:
{
  "id": "access_request_001",
  "status": "approved",
  "approvedBy": {
    "id": "staff_owner_001",
    "name": "원장님"
  },
  "approvedUntil": "2026-09-16T20:00:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 이미 처리된 요청: “이미 승인 또는 거절된 요청입니다.”
    • 승인 시간이 현재보다 이전: “임시 열람 종료 시간을 다시 확인해 주세요.”

확인 필요: FR-006의 구현 방식은 임시 열람 요청 후 원장님 승인 방식으로 연결했다. 원장님이 직접 일정 시간 권한을 부여하는 방식 또는 담당 디자이너 변경 방식으로 확정하면 이 API를 해당 정책에 맞게 조정한다.


5. 시술 기록과 시술 사진

POST /api/treatment-records

  • 목적: 실제 시술 내용, 염색약 번호, 배합, 메모를 저장하고 필요하면 다음 예약을 함께 등록한다. (FR-008, FR-018, UIR-005)
  • 인증: 필요
  • 요청값:
{
  "customerId": "customer_001",
  "reservationConfirmationId": "reservation_001",
  "treatedAt": "2026-09-16T14:00:00+09:00",
  "designerId": "staff_123",
  "treatmentType": "염색",
  "treatmentMemo": "뿌리 염색, 붉은기 보정",
  "dyeNumber": "7N",
  "formula": "7N 30g + 7A 10g + 6% 산화제 60g",
  "nextReservation": {
    "scheduledAt": "2026-11-16T14:00:00+09:00",
    "treatmentType": "뿌리 염색",
    "contactPoint": "매장·현장 방문"
  }
}
  • 성공 응답:
{
  "id": "treatment_001",
  "customerId": "customer_001",
  "reservationConfirmationId": "reservation_001",
  "treatedAt": "2026-09-16T14:00:00+09:00",
  "treatmentType": "염색",
  "treatmentMemo": "뿌리 염색, 붉은기 보정",
  "nextReservationConfirmationId": "reservation_002",
  "createdAt": "2026-09-16T15:00:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 시술 종류 또는 시술 메모 미입력: “시술 종류와 시술 메모를 입력해 주세요.”
    • 다른 가게 고객 또는 예약 확인 건 연결: “현재 가게의 고객과 예약만 연결할 수 있습니다.”
    • 권한 없는 고객 기록 작성: “이 고객의 시술 기록을 작성할 권한이 없습니다.”
    • 동시 수정 충돌: “다른 사용자가 먼저 수정했습니다. 최신 내용을 확인해 주세요.”

PATCH /api/treatment-records/{treatmentRecordId}

  • 목적: 기존 시술 기록의 시술 내용, 염색약 번호, 배합, 메모를 수정하고 수정일을 남긴다. (FR-008)
  • 인증: 필요
  • 요청값:
{
  "treatmentType": "염색",
  "treatmentMemo": "뿌리 염색, 붉은기 보정 완료",
  "dyeNumber": "7N",
  "formula": "7N 30g + 7A 10g + 6% 산화제 60g",
  "version": 3
}
  • 성공 응답:
{
  "id": "treatment_001",
  "version": 4,
  "updatedAt": "2026-09-16T15:20:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 최신 버전과 다름: “기록이 수정되었습니다. 최신 내용을 확인한 뒤 다시 저장해 주세요.”
    • 시술 기록 없음: “시술 기록을 찾을 수 없습니다.”

POST /api/treatment-records/{treatmentRecordId}/photos

  • 목적: 촬영·보관 동의와 보유기간이 확인된 고객의 시술 기록에 사진을 연결한다. (FR-009)
  • 인증: 필요
  • 요청값: multipart/form-data
항목필수설명
file시술 사진 파일
caption아니오사진 설명
  • 성공 응답:
{
  "id": "photo_001",
  "treatmentRecordId": "treatment_001",
  "fileName": "treatment-photo.jpg",
  "uploadedAt": "2026-09-16T15:30:00+09:00",
  "retentionUntil": "2027-09-16"
}
  • 사용자에게 표시할 오류 상황:
    • 촬영·보관 동의 없음 또는 미확인: “시술 사진 촬영·보관 동의를 확인한 뒤 첨부할 수 있습니다.”
    • 보유기간 미설정: “사진 보유기간을 먼저 설정해 주세요.”
    • 지원하지 않는 파일 또는 업로드 실패: “사진을 올리지 못했습니다. 파일 형식을 확인해 주세요.”

DELETE /api/treatment-records/{treatmentRecordId}/photos/{photoId}

  • 목적: 시술 기록에 연결된 사진을 삭제하고 삭제 이력을 남긴다. (FR-009, FR-028)
  • 인증: 필요
  • 요청값: 없음
  • 성공 응답:
{
  "success": true,
  "deletedAt": "2026-09-16T15:40:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 권한 없는 사진 삭제: “이 사진을 삭제할 권한이 없습니다.”
    • 이미 삭제된 사진: “이미 삭제된 사진입니다.”

6. 예약 확인 건과 오늘·내일 예약

GET /api/reservation-confirmations

  • 목적: 오늘·내일 예약 확인 화면과 고객 상세에서 예약 확인 건을 조회한다. (FR-005, FR-010, FR-011, UIR-002, UIR-004)
  • 인증: 필요
  • 요청값:
    • from: 시작 일시
    • to: 종료 일시
    • customerId: 선택
    • designerId: 선택. 원장님만 다른 디자이너 조건 조회 가능
    • visitResult: 선택. pending, completed, no_show, customer_cancelled, store_cancelled
  • 성공 응답:
{
  "items": [
    {
      "id": "reservation_001",
      "scheduledAt": "2026-09-16T14:00:00+09:00",
      "customer": {
        "id": "customer_001",
        "name": "김고객",
        "displayPhone": "010-1234-5678"
      },
      "designer": {
        "id": "staff_123",
        "name": "김디자이너"
      },
      "treatmentType": "염색",
      "contactPoint": "네이버 예약",
      "customerConfirmationStatus": "confirmed",
      "visitResult": "pending",
      "operationalMessages": {
        "dayBefore": "success",
        "sameDay": "scheduled"
      }
    }
  ],
  "page": 1,
  "pageSize": 50,
  "total": 1
}
  • 사용자에게 표시할 오류 상황:
    • 알림톡 설정 미완료: 목록 상단에 “알림톡 자동 발송 설정이 완료되지 않았습니다.”를 표시한다.
    • 발송 실패 예약 존재: 해당 예약을 “수동 연락 필요”로 표시한다.

POST /api/reservation-confirmations

  • 목적: 네이버 예약을 대체하지 않고 예약 확인과 안내 발송에 필요한 최소 예약 정보를 직접 등록한다. (FR-010, FR-011)
  • 인증: 필요
  • 요청값:
{
  "customerId": "customer_001",
  "scheduledAt": "2026-09-17T14:00:00+09:00",
  "designerId": "staff_123",
  "treatmentType": "염색",
  "contactPoint": "네이버 예약",
  "customerConfirmationStatus": "not_contacted"
}
  • 성공 응답:
{
  "id": "reservation_002",
  "customerId": "customer_001",
  "scheduledAt": "2026-09-17T14:00:00+09:00",
  "contactSnapshot": {
    "name": "김고객",
    "displayPhone": "010-1234-5678",
    "normalizedPhone": "01012345678"
  },
  "messageSchedule": {
    "dayBeforeAt": "2026-09-16T18:00:00+09:00",
    "sameDayAt": "2026-09-17T11:00:00+09:00"
  },
  "autoMessageEligibility": "eligible"
}
  • 사용자에게 표시할 오류 상황:
    • 과거 방문 일시 입력: “지난 시간은 새 예약으로 등록할 수 없습니다.”
    • 전화번호 검토 필요 고객: “연락처 확인이 필요합니다. 자동 알림톡은 예약되지 않습니다.”
    • 같은 고객·방문 일시·담당 디자이너 예약 존재: “비슷한 예약이 이미 있습니다. 중복 등록인지 확인해 주세요.”
    • 권한 없는 다른 담당 고객 등록: “이 고객의 예약을 등록할 권한이 없습니다.”

PATCH /api/reservation-confirmations/{reservationId}

  • 목적: 예약 확인 건의 방문 예정 일시, 담당 디자이너, 예정 시술, 고객 확인 상태, 방문 결과를 수정한다. 변경 시 미발송 메시지 일정을 다시 계산한다. (FR-010, FR-011, FR-014, FR-017)
  • 인증: 필요
  • 요청값:
{
  "scheduledAt": "2026-09-17T15:00:00+09:00",
  "designerId": "staff_123",
  "treatmentType": "염색",
  "customerConfirmationStatus": "confirmed",
  "visitResult": "completed"
}
  • 성공 응답:
{
  "id": "reservation_002",
  "scheduledAt": "2026-09-17T15:00:00+09:00",
  "visitResult": "completed",
  "messageSchedule": {
    "dayBefore": "success",
    "sameDay": "rescheduled"
  },
  "treatmentRecordTask": {
    "status": "open",
    "assigneeId": "staff_123"
  }
}
  • 사용자에게 표시할 오류 상황:
    • 이미 취소된 예약에 시술 완료 입력: “취소된 예약은 시술 완료로 바꿀 수 없습니다.”
    • 예약 시간 경과 후 당일 안내 발송 요청: “방문 시간이 지나 당일 안내를 보낼 수 없습니다.”
    • 권한 없음: “이 예약 확인 건을 수정할 권한이 없습니다.”

POST /api/reservation-confirmations/{reservationId}/manual-contacts

  • 목적: 알림톡 발송 실패 또는 연결 불가 시 전화, 카카오톡 채널 등으로 수동 연락한 결과를 기록한다. (FR-016)
  • 인증: 필요
  • 요청값:
{
  "contactedAt": "2026-09-16T18:15:00+09:00",
  "channel": "전화",
  "result": "통화 완료, 방문 확인",
  "customerConfirmationStatus": "confirmed"
}
  • 성공 응답:
{
  "id": "manual_contact_001",
  "reservationConfirmationId": "reservation_002",
  "status": "manual_contact_completed",
  "contactedAt": "2026-09-16T18:15:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 연락 일시 미입력: “연락한 일시를 입력해 주세요.”
    • 연락 결과 미입력: “연락 결과를 입력해 주세요.”
    • 취소된 예약: “취소된 예약에는 수동 연락 결과를 추가할 수 없습니다.”

GET /api/tasks/treatment-records

  • 목적: 방문 결과가 시술 완료인데 연결된 시술 기록이 없는 예약 확인 건을 담당 디자이너의 내부 할 일로 보여준다. (FR-017, UIR-002)
  • 인증: 필요
  • 요청값:
    • status: 선택. 기본값 open
  • 성공 응답:
{
  "items": [
    {
      "id": "task_001",
      "type": "treatment_record_required",
      "reservationConfirmationId": "reservation_002",
      "customerName": "김고객",
      "scheduledAt": "2026-09-17T15:00:00+09:00",
      "assigneeName": "김디자이너",
      "status": "open"
    }
  ],
  "page": 1,
  "pageSize": 20,
  "total": 1
}
  • 사용자에게 표시할 오류 상황:
    • 권한 없는 할 일 조회: 자기 담당 예약의 할 일만 표시한다.

7. 알림톡 설정, 발송과 상태 반영

GET /api/alimtalk/settings

  • 목적: 발신 프로필, 전날 예약 확인 템플릿, 당일 예약 안내 템플릿의 연결 상태를 조회한다. (FR-015, UIR-012)
  • 인증: 필요, 원장님만 가능
  • 요청값: 없음
  • 성공 응답:
{
  "senderProfile": {
    "status": "connected",
    "profileName": "온헤어",
    "updatedAt": "2026-09-01T10:00:00+09:00"
  },
  "templates": {
    "dayBeforeConfirmation": {
      "status": "approved",
      "templateCode": "ONHAIR_RESERVATION_CONFIRM"
    },
    "sameDayReminder": {
      "status": "approved",
      "templateCode": "ONHAIR_SAME_DAY_NOTICE"
    }
  },
  "automaticSendingEnabled": true
}
  • 사용자에게 표시할 오류 상황:
    • 발신 프로필 또는 템플릿 미연결: “알림톡 자동 발송을 위해 발신 프로필과 템플릿을 확인해 주세요.”

PATCH /api/alimtalk/settings

  • 목적: 알림톡 발신 프로필과 예약 안내용 템플릿 상태를 저장하고 자동 발송 가능 여부를 관리한다. (FR-015, UIR-012)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "senderProfileKey": "provider-profile-key",
  "dayBeforeTemplateCode": "ONHAIR_RESERVATION_CONFIRM",
  "sameDayTemplateCode": "ONHAIR_SAME_DAY_NOTICE",
  "automaticSendingEnabled": true
}
  • 성공 응답:
{
  "senderProfile": {
    "status": "verification_pending"
  },
  "templates": {
    "dayBeforeConfirmation": {
      "status": "verification_pending"
    },
    "sameDayReminder": {
      "status": "verification_pending"
    }
  }
}
  • 사용자에게 표시할 오류 상황:
    • 발신 프로필 또는 템플릿 정보가 유효하지 않음: “알림톡 연결 정보를 확인해 주세요.”
    • 템플릿에 필수 안내 항목 없음: “예약 일시, 담당 디자이너, 예정 시술, 변경·취소 안내가 포함된 템플릿이 필요합니다.”

POST /api/internal/jobs/send-day-before-alimtalk

  • 목적: 매일 전날 오후 6시에 다음 날 예약 확인 알림톡 발송 대상을 찾아 한 번씩 발송한다. (FR-012, NFR-004)
  • 인증: 내부 작업 인증 필요
  • HTTP 방식과 경로: POST /api/internal/jobs/send-day-before-alimtalk
  • 요청값:
{
  "runAt": "2026-09-16T18:00:00+09:00"
}
  • 성공 응답:
{
  "processedCount": 15,
  "successCount": 13,
  "failedCount": 1,
  "stoppedCount": 1
}
  • 사용자에게 표시할 오류 상황:
    • 이 API는 직원 화면에서 직접 실행하지 않는다.
    • 실패 또는 자동 발송 중지 건은 오늘·내일 예약 확인 화면에서 “수동 연락 필요”로 표시한다.

POST /api/internal/jobs/send-same-day-alimtalk

  • 목적: 각 예약의 방문 3시간 전에 당일 예약 안내 알림톡을 한 번씩 발송한다. (FR-013, NFR-004)
  • 인증: 내부 작업 인증 필요
  • 요청값:
{
  "runAt": "2026-09-17T11:00:00+09:00"
}
  • 성공 응답:
{
  "processedCount": 8,
  "successCount": 7,
  "failedCount": 1,
  "skippedPastVisitCount": 0
}
  • 사용자에게 표시할 오류 상황:
    • 방문 시간이 지난 예약은 발송하지 않고 목록에 “발송 시간 지남”으로 표시한다.
    • 번호 오류, 병합 검토 중, 알림톡 연결 오류는 수동 연락 대상으로 표시한다.

POST /api/webhooks/alimtalk/status

  • 목적: 알림톡 제공업체가 전달하는 처리 중, 성공, 실패 결과를 운영 메시지 상태에 반영한다. (FR-014)
  • 인증: 알림톡 제공업체 서명 또는 비밀키 검증 필요
  • 요청값:
{
  "requestId": "alimtalk_request_001",
  "resultId": "provider_result_001",
  "status": "success",
  "occurredAt": "2026-09-16T18:00:03+09:00",
  "failureReason": null
}
  • 성공 응답:
{
  "success": true
}
  • 사용자에게 표시할 오류 상황:
    • 연결되지 않은 요청 식별값: 운영자 확인용 오류 기록을 남긴다.
    • 실패 결과: 예약 확인 건에 “알림톡 실패”와 실패 원인을 표시하고 수동 연락 대상으로 전환한다.

8. 고객 단계와 뜸해진 고객

GET /api/dormant-customers

  • 목적: 마지막 시술 후 달력 기준 2개월이 지났고 미래 예약이 없는 고객을 담당 디자이너별로 조회한다. 마케팅 수신동의와 마지막 연락 내용을 함께 표시한다. (FR-019, FR-020, UIR-006)
  • 인증: 필요
  • 요청값:
    • designerId: 선택. 원장님만 다른 담당자 조건 사용 가능
    • marketingConsentStatus: 선택. agreed, declined, unconfirmed
    • page, pageSize
  • 성공 응답:
{
  "items": [
    {
      "customerId": "customer_001",
      "name": "김고객",
      "displayPhone": "010-1234-5678",
      "assignedDesignerName": "김디자이너",
      "lastTreatmentAt": "2026-07-10T14:00:00+09:00",
      "dormantSince": "2026-09-10T00:00:00+09:00",
      "marketingConsentStatus": "agreed",
      "lastContact": {
        "contactedAt": "2026-09-12T13:00:00+09:00",
        "channel": "카카오톡 채널",
        "result": "응답 없음"
      }
    }
  ],
  "page": 1,
  "pageSize": 20,
  "total": 1
}
  • 사용자에게 표시할 오류 상황:
    • 디자이너는 자기 담당 고객만 표시한다.
    • 마케팅 수신동의가 거부 또는 미확인인 고객은 상태를 분명히 표시한다.

POST /api/customers/{customerId}/dormant-contacts

  • 목적: 뜸해진 고객에게 연락한 일시, 연락 채널, 연락 결과를 기록한다. (FR-021, UIR-006)
  • 인증: 필요
  • 요청값:
{
  "contactedAt": "2026-09-16T16:00:00+09:00",
  "channel": "카카오톡 채널",
  "result": "다음 주 예약 문의 예정"
}
  • 성공 응답:
{
  "id": "dormant_contact_001",
  "customerId": "customer_001",
  "contactedAt": "2026-09-16T16:00:00+09:00",
  "channel": "카카오톡 채널",
  "result": "다음 주 예약 문의 예정"
}
  • 사용자에게 표시할 오류 상황:
    • 연락 채널 또는 결과 미입력: “연락 채널과 결과를 입력해 주세요.”
    • 뜸해진 고객이 아님: “현재 뜸해진 고객 목록에 없는 고객입니다.”

POST /api/internal/jobs/recalculate-customer-stages

  • 목적: 예약 확인 건과 시술 기록 변경 뒤 고객 단계를 자동 계산하고, 뜸해짐 대상 여부를 갱신한다. (FR-019, FR-020)
  • 인증: 내부 작업 인증 필요
  • 요청값:
{
  "customerId": "customer_001",
  "trigger": "treatment_record_created"
}
  • 성공 응답:
{
  "customerId": "customer_001",
  "previousStage": "completed_treatment",
  "currentStage": "next_reservation_booked",
  "isDormant": false
}
  • 사용자에게 표시할 오류 상황:
    • 이 작업의 오류는 고객 상세와 뜸해진 고객 목록에서 최신 계산이 지연될 수 있음을 운영자에게 표시한다.

9. CSV 자료 가져오기와 수첩 자료 입력

POST /api/import-jobs

  • 목적: 고객 정보, 방문 이력, 시술 기록, 예약 확인 건 CSV 파일을 올려 임시 검증 작업을 시작한다. (FR-022, FR-023, UIR-008)
  • 인증: 필요, 원장님만 가능
  • 요청값: multipart/form-data
항목필수설명
fileCSV 파일
dataTypecustomers, treatment_records, reservation_confirmations
hasHeader첫 행 제목 포함 여부
  • 성공 응답:
{
  "id": "import_001",
  "status": "validating",
  "dataType": "treatment_records",
  "uploadedFileName": "onhair-treatment-history.csv"
}
  • 사용자에게 표시할 오류 상황:
    • CSV가 아닌 파일: “CSV 파일만 가져올 수 있습니다.”
    • 지원하지 않는 열 구조: “필수 열 이름 또는 열 순서를 확인해 주세요.”
    • 파일 처리 실패: “자료를 읽지 못했습니다. 파일을 다시 확인해 주세요.”

GET /api/import-jobs/{importJobId}

  • 목적: 자료 가져오기 검증 결과, 정상 행 수, 오류 행 수, 중복 후보를 확인한다. (FR-022, FR-023, UIR-008)
  • 인증: 필요, 원장님만 가능
  • 요청값: 없음
  • 성공 응답:
{
  "id": "import_001",
  "status": "validation_completed",
  "summary": {
    "totalRows": 300,
    "validRows": 280,
    "errorRows": 12,
    "duplicateCandidateRows": 8
  },
  "errors": [
    {
      "rowNumber": 21,
      "field": "phone",
      "reason": "휴대전화 번호 형식을 확인할 수 없습니다."
    }
  ]
}
  • 사용자에게 표시할 오류 상황:
    • 가져오기 작업 없음: “자료 가져오기 작업을 찾을 수 없습니다.”
    • 아직 검증 중: “자료를 확인하고 있습니다. 잠시 후 다시 확인해 주세요.”

PATCH /api/import-jobs/{importJobId}/error-rows/{rowNumber}

  • 목적: 전화번호, 날짜, 담당 디자이너 연결 오류가 있는 행을 수정하고 다시 검증한다. (FR-023)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "values": {
    "customerName": "김고객",
    "phone": "010-1234-5678",
    "designerName": "김디자이너",
    "treatedAt": "2026-08-10T14:00:00+09:00"
  }
}
  • 성공 응답:
{
  "rowNumber": 21,
  "status": "valid",
  "duplicateCheckStatus": "no_duplicate"
}
  • 사용자에게 표시할 오류 상황:
    • 수정 후에도 전화번호 또는 날짜가 유효하지 않음: “수정한 값을 다시 확인해 주세요.”
    • 담당자 이름을 연결할 수 없음: “등록된 직원 중 담당 디자이너를 선택해 주세요.”

POST /api/import-jobs/{importJobId}/apply

  • 목적: 검증이 끝난 정상 행만 실제 고객, 시술 기록, 예약 확인 건으로 반영한다. 반영 과정에서 중복 생성을 막는다. (FR-022, FR-023, NFR-006)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "applyValidRowsOnly": true
}
  • 성공 응답:
{
  "id": "import_001",
  "status": "applied",
  "createdCustomers": 120,
  "linkedExistingCustomers": 150,
  "createdTreatmentRecords": 280,
  "skippedErrorRows": 12
}
  • 사용자에게 표시할 오류 상황:
    • 검증되지 않은 행 존재: “오류 행을 수정하거나 정상 행만 반영하도록 선택해 주세요.”
    • 이미 반영한 작업 재실행: “이미 반영된 자료입니다. 중복으로 가져올 수 없습니다.”

POST /api/customers/manual-entry

  • 목적: 수첩에 있는 고객 정보와 과거 방문·시술 기록을 직접 입력한다. (FR-022)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "customer": {
    "name": "박고객",
    "rawPhone": "010-9876-5432",
    "assignedDesignerId": "staff_123",
    "privacyConsent": {
      "status": "unconfirmed",
      "channel": "기존 수첩"
    },
    "marketingConsent": {
      "status": "unconfirmed",
      "channel": "기존 수첩"
    }
  },
  "treatmentRecords": [
    {
      "treatedAt": "2026-06-15T14:00:00+09:00",
      "designerId": "staff_123",
      "treatmentType": "염색",
      "treatmentMemo": "전체 염색",
      "dyeNumber": "6N",
      "formula": "6N 40g + 산화제 80g"
    }
  ]
}
  • 성공 응답:
{
  "customerId": "customer_020",
  "createdTreatmentRecordCount": 1,
  "duplicateCheckStatus": "no_duplicate"
}
  • 사용자에게 표시할 오류 상황:
    • 같은 전화번호 고객 존재: “기존 고객이 있습니다. 새로 만들지 않고 기존 고객과 연결해 주세요.”
    • 시술 종류 또는 시술 메모 누락: “시술 기록에는 시술 종류와 메모가 필요합니다.”

10. 중복 고객 병합과 병합 취소

GET /api/customer-merge-candidates

  • 목적: 같은 정규화 번호 또는 자료 가져오기 과정에서 발견된 중복 고객 후보를 조회한다. (FR-024, UIR-010)
  • 인증: 필요
    • 원장님: 전체 후보 조회 가능
    • 디자이너: 중복 후보 제시 범위만 가능
  • 요청값:
    • status: pending, merged, ignored
    • source: 선택. manual, import
  • 성공 응답:
{
  "items": [
    {
      "id": "merge_candidate_001",
      "reason": "같은 정규화 번호",
      "customers": [
        {
          "id": "customer_001",
          "name": "김고객",
          "displayPhone": "010-1234-5678",
          "assignedDesignerName": "김디자이너"
        },
        {
          "id": "customer_098",
          "name": "김OO",
          "displayPhone": "010-1234-5678",
          "assignedDesignerName": "이디자이너"
        }
      ],
      "status": "pending"
    }
  ],
  "page": 1,
  "pageSize": 20,
  "total": 1
}
  • 사용자에게 표시할 오류 상황:
    • 다른 가게 고객은 후보로 함께 표시하지 않는다.
    • 디자이너는 병합 실행 권한이 없음을 표시한다.

POST /api/customer-merges

  • 목적: 원장님이 대표 고객과 통합 대상 고객을 확인한 뒤 고객, 시술 기록, 예약 확인 건, 동의 이력을 안전하게 병합한다. (FR-024, NFR-005)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "primaryCustomerId": "customer_001",
  "mergedCustomerId": "customer_098",
  "resolution": {
    "nameSource": "primary",
    "assignedDesignerId": "staff_123",
    "privacyConsentSource": "latest_history",
    "marketingConsentSource": "latest_history"
  },
  "reason": "같은 휴대전화 번호의 기존 고객"
}
  • 성공 응답:
{
  "mergeId": "merge_001",
  "primaryCustomerId": "customer_001",
  "mergedCustomerId": "customer_098",
  "movedTreatmentRecordCount": 4,
  "movedReservationConfirmationCount": 2,
  "mergedAt": "2026-09-16T17:00:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 동일 고객을 대표와 통합 대상으로 선택: “서로 다른 고객을 선택해 주세요.”
    • 이미 병합된 고객 다시 병합: “이미 통합된 고객입니다.”
    • 다른 가게 고객 병합 시도: “같은 가게의 고객만 병합할 수 있습니다.”

GET /api/customer-merges

  • 목적: 고객 병합 이력과 대표 고객, 통합 대상 고객, 실행자, 실행 시각을 조회한다. (FR-025, UIR-011)
  • 인증: 필요, 원장님만 가능
  • 요청값:
    • customerId: 선택
    • page, pageSize
  • 성공 응답:
{
  "items": [
    {
      "id": "merge_001",
      "primaryCustomer": {
        "id": "customer_001",
        "name": "김고객"
      },
      "mergedCustomer": {
        "id": "customer_098",
        "name": "김OO"
      },
      "mergedBy": {
        "id": "staff_owner_001",
        "name": "원장님"
      },
      "mergedAt": "2026-09-16T17:00:00+09:00",
      "canUndo": true
    }
  ],
  "page": 1,
  "pageSize": 20,
  "total": 1
}
  • 사용자에게 표시할 오류 상황:
    • 병합 이력 없음: “병합 이력이 없습니다.”

POST /api/customer-merges/{mergeId}/undo

  • 목적: 병합 당시 저장한 연결 관계와 값으로 고객 병합을 취소한다. (FR-025, NFR-005)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "reason": "서로 다른 가족 고객으로 확인됨"
}
  • 성공 응답:
{
  "mergeId": "merge_001",
  "status": "undone",
  "restoredCustomerId": "customer_098",
  "undoneAt": "2026-09-16T17:20:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 병합 후 새 기록이 추가되어 자동 복구 불가: “병합 후 추가된 기록이 있어 자동 취소할 수 없습니다. 원장님이 기록을 확인해 주세요.”
    • 이미 취소된 병합: “이미 취소된 병합입니다.”

11. 동의, 보유기간, 삭제 요청

GET /api/privacy/settings

  • 목적: 가게의 개인정보 보유기간과 사진 보유기간 설정을 조회한다. (FR-026, FR-027, UIR-007)
  • 인증: 필요, 원장님만 가능
  • 요청값: 없음
  • 성공 응답:
{
  "customerRetentionMonths": 60,
  "treatmentPhotoRetentionMonths": 12,
  "lastUpdatedAt": "2026-09-01T10:00:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 설정값이 없는 경우: “개인정보와 시술 사진 보유기간을 설정해 주세요.”

PATCH /api/privacy/settings

  • 목적: 고객 개인정보와 시술 사진의 보유기간을 설정한다. (FR-027, UIR-007)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "customerRetentionMonths": 60,
  "treatmentPhotoRetentionMonths": 12
}
  • 성공 응답:
{
  "customerRetentionMonths": 60,
  "treatmentPhotoRetentionMonths": 12,
  "updatedAt": "2026-09-16T17:30:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 0 이하 값 입력: “보유기간은 1개월 이상으로 입력해 주세요.”

GET /api/privacy/retention-targets

  • 목적: 마지막 방문일과 보유기간을 기준으로 삭제 또는 알아볼 수 없게 처리해야 하는 고객을 찾는다. (FR-027, UIR-007)
  • 인증: 필요, 원장님만 가능
  • 요청값:
    • status: due, requested, processed
  • 성공 응답:
{
  "items": [
    {
      "customerId": "customer_005",
      "name": "최고객",
      "lastTreatmentAt": "2021-08-10T14:00:00+09:00",
      "retentionDueAt": "2026-08-10T00:00:00+09:00",
      "reason": "보유기간 경과"
    }
  ],
  "page": 1,
  "pageSize": 20,
  "total": 1
}
  • 사용자에게 표시할 오류 상황:
    • 대상 없음: “처리할 고객이 없습니다.”

POST /api/customers/{customerId}/deletion-requests

  • 목적: 고객의 삭제 요청 또는 보유기간 경과에 따라 삭제·비식별 처리 작업을 등록한다. (FR-027)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "reason": "customer_request",
  "requestedAt": "2026-09-16T17:40:00+09:00",
  "note": "전화로 삭제 요청"
}
  • 성공 응답:
{
  "id": "deletion_request_001",
  "customerId": "customer_001",
  "status": "pending"
}
  • 사용자에게 표시할 오류 상황:
    • 이미 처리 중인 요청: “이미 삭제 처리 중인 고객입니다.”
    • 고객 없음: “고객을 찾을 수 없습니다.”

POST /api/deletion-requests/{deletionRequestId}/process

  • 목적: 고객의 개인정보를 삭제하거나 알아볼 수 없게 처리하고 처리 이력을 남긴다. (FR-027, FR-028)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "method": "anonymize"
}
  • 성공 응답:
{
  "id": "deletion_request_001",
  "status": "processed",
  "processedAt": "2026-09-16T18:00:00+09:00",
  "customerRecordStatus": "anonymized"
}
  • 사용자에게 표시할 오류 상황:
    • 이미 처리된 요청: “이미 삭제 또는 비식별 처리된 고객입니다.”
    • 처리 중 오류: “고객 정보 처리에 실패했습니다. 다시 시도해 주세요.”

12. 활동 이력과 운영 지표

GET /api/activity-logs

  • 목적: 고객 조회·수정·내보내기, 권한 변경, 병합, 삭제 처리 이력을 원장님이 확인한다. (FR-028, UIR-007)
  • 인증: 필요, 원장님만 가능
  • 요청값:
    • action: 선택. view, update, export, permission_change, merge, delete
    • actorId: 선택
    • targetType: 선택. customer, treatment_record, staff, export
    • from, to
    • page, pageSize
  • 성공 응답:
{
  "items": [
    {
      "id": "activity_001",
      "action": "view",
      "actor": {
        "id": "staff_123",
        "name": "김디자이너"
      },
      "targetType": "customer",
      "targetId": "customer_001",
      "occurredAt": "2026-09-16T14:00:00+09:00",
      "reason": "대체 시술 임시 열람 승인"
    }
  ],
  "page": 1,
  "pageSize": 50,
  "total": 1
}
  • 사용자에게 표시할 오류 상황:
    • 디자이너 접근: “활동 이력은 원장님만 확인할 수 있습니다.”

POST /api/customers/export

  • 목적: 원장님이 선택한 고객 목록을 CSV로 내보내고 내보내기 이력을 남긴다. (FR-028)
  • 인증: 필요, 원장님만 가능
  • 요청값:
{
  "customerIds": ["customer_001", "customer_002"],
  "fields": [
    "name",
    "displayPhone",
    "assignedDesigner",
    "stage",
    "lastTreatmentAt",
    "marketingConsentStatus"
  ],
  "reason": "월간 고객 현황 확인"
}
  • 성공 응답:
{
  "exportId": "export_001",
  "downloadUrl": "서명된-임시-다운로드-주소",
  "expiresAt": "2026-09-16T18:30:00+09:00"
}
  • 사용자에게 표시할 오류 상황:
    • 내보낼 고객 미선택: “내보낼 고객을 선택해 주세요.”
    • 민감한 정보 또는 권한 없는 범위 요청: “내보낼 수 없는 정보가 포함되어 있습니다.”

GET /api/metrics/operations

  • 목적: 노쇼율, 예약 확인 안내 발송률, 두 달 내 재방문율, 시술 기록 완료율, 시술 직후 재예약률을 계산해 보여준다. (FR-029)
  • 인증: 필요, 원장님만 가능
  • 요청값:
    • from: 분석 시작일
    • to: 분석 종료일
    • designerId: 선택
  • 성공 응답:
{
  "period": {
    "from": "2026-08-01",
    "to": "2026-08-31"
  },
  "metrics": {
    "noShowRate": {
      "value": 4.2,
      "numerator": 2,
      "denominator": 48
    },
    "reservationMessageDeliveryRate": {
      "value": 95.8,
      "numerator": 46,
      "denominator": 48
    },
    "twoMonthRevisitRate": {
      "value": 38.5,
      "numerator": 10,
      "denominator": 26
    },
    "treatmentRecordCompletionRate": {
      "value": 93.8,
      "numerator": 45,
      "denominator": 48
    },
    "immediateRebookingRate": {
      "value": 41.7,
      "numerator": 20,
      "denominator": 48
    }
  }
}
  • 사용자에게 표시할 오류 상황:
    • 관찰 기간이 충분하지 않은 재방문율: “두 달 관찰 기간이 끝난 시술 기록이 부족해 재방문율을 계산할 수 없습니다.”
    • 조회 기간이 잘못됨: “시작일은 종료일보다 이전이어야 합니다.”

구현 시 반드시 지킬 기준

항목구현 기준관련 요구사항
가게 데이터 분리모든 조회와 수정은 서버에서 storeId를 기준으로 제한한다. 요청값의 가게 식별값을 신뢰하지 않는다.FR-001, FR-005, NFR-002
디자이너 권한디자이너는 자기 담당 고객, 예약 확인 건, 시술 기록만 기본 접근 가능하다.FR-005, NFR-003
대체 시술 열람다른 담당 고객의 상세 시술 이력은 FR-006에서 확정한 임시 열람 정책과 이력 기록을 거쳐야 한다.FR-006, FR-028
중복 발송 방지예약 확인 건별로 안내 종류 + 예약 확인 건 식별값을 고유하게 저장해 알림톡 중복 발송을 막는다.FR-012, FR-013, FR-014, NFR-004
예약 시간 변경이미 성공한 알림톡은 다시 보내지 않는다. 아직 발송되지 않은 안내만 새 예약 시간 기준으로 다시 계산한다.FR-014
고객 병합병합 전 고객 값, 연결된 시술 기록·예약 확인 건의 기존 연결, 동의 이력을 보관해야 병합 취소가 가능하다.FR-024, FR-025, NFR-005
자료 가져오기CSV는 검증 완료 전 실제 데이터에 반영하지 않는다. 오류 행은 원본 행 번호와 오류 이유를 보관한다.FR-022, FR-023, NFR-006
개인정보개인정보 동의와 마케팅 수신동의는 별도 상태와 변경 이력을 가진다.FR-026, NFR-003
시술 사진사진은 촬영·보관 동의와 보유기간이 있을 때만 업로드한다.FR-009, FR-027
오류 복구저장 실패, 발송 실패, 가져오기 오류는 사용자가 원인과 다음 행동을 이해할 수 있게 표시한다.NFR-010
기능 연결 방식 — 동네 헤어샵의 시술 이력과 예약 확인 | Prometheon