Skip to content

DDINGJOO/SUPPORT_SERVER

Repository files navigation

Support-Server 가이드 문서

1. 개요

1.1 목적

Support-Server는 플랫폼 내 고객지원 시스템을 담당하는 마이크로서비스이다. 신고 관리, 1:1 문의, FAQ 기능을 통해 사용자 지원 업무를 처리한다.

1.2 주요 기능

기능 설명
신고 관리 프로필/게시글/업체 신고 접수 및 처리
제재 시스템 신고 기반 경고/정지/영구정지 제재 적용
1:1 문의 카테고리별 문의 등록 및 답변 관리
FAQ 인메모리 캐싱 기반 자주 묻는 질문 제공
신고 통계 신고 현황 집계 및 분석
커서 페이징 대용량 데이터 효율적 조회

1.3 기술 스택

구분 기술
Framework Spring Boot 3.5.6
Language Java 21 (Eclipse Temurin)
Database MariaDB 11.x
Cache Redis 7.x (선택)
Query QueryDSL 5.0.0
ID 생성 Snowflake Algorithm
Build Tool Gradle 8.x
Test Coverage JaCoCo 0.8.11
Documentation Swagger/OpenAPI 3.0

2. 시스템 아키텍처

2.1 전체 구조

flowchart TB
    subgraph Client
        APP[Mobile App]
        WEB[Web Client]
        ADMIN[Admin Console]
    end

    subgraph Load_Balancer
        NGINX[Nginx :9100]
    end

    subgraph Support_Service
        SUPPORT1[Support Server #1]
        SUPPORT2[Support Server #2]
        SUPPORT3[Support Server #3]
    end

    subgraph Data_Layer
        MARIA[(MariaDB)]
        REDIS[(Redis)]
    end

    subgraph Downstream_Services
        AUTH[Auth Server]
        PROFILE[Profile Server]
        NOTI[Notification Server]
    end

    APP --> NGINX
    WEB --> NGINX
    ADMIN --> NGINX
    NGINX --> SUPPORT1
    NGINX --> SUPPORT2
    NGINX --> SUPPORT3
    SUPPORT1 --> MARIA
    SUPPORT1 --> REDIS
    SUPPORT1 -.-> AUTH
    SUPPORT1 -.-> PROFILE
    SUPPORT1 -.-> NOTI
Loading

2.2 레이어 아키텍처

flowchart TB
    subgraph Presentation["Presentation Layer"]
        CTRL[Controllers]
        DTO[Request/Response DTOs]
        VALID[Validators]
    end

    subgraph Application["Application Layer"]
        SVC[Services]
        CACHE[Cache Management]
    end

    subgraph Domain["Domain Layer"]
        ENT[Entities]
        REPO[Repositories]
        ENUM[Enums]
    end

    subgraph Infrastructure["Infrastructure Layer"]
        DB[(MariaDB)]
        MEM[In-Memory Cache]
        QDSL[QueryDSL]
    end

    CTRL --> SVC
    SVC --> REPO
    SVC --> CACHE
    REPO --> DB
    REPO --> QDSL
    CACHE --> MEM
Loading

2.3 신고 처리 흐름

sequenceDiagram
    participant U as User
    participant LB as Nginx LB
    participant SS as Support Server
    participant DB as MariaDB
    participant ADMIN as Admin

    U->>LB: POST /api/v1/reports
    LB->>SS: 신고 요청 전달
    SS->>SS: 입력값 검증
    SS->>SS: 카테고리 존재 확인 (캐시)
    SS->>SS: Snowflake ID 생성
    SS->>DB: Report 저장 (PENDING)
    SS-->>LB: ReportResponse
    LB-->>U: 신고 접수 완료

    ADMIN->>SS: PATCH /api/v1/reports/{id}
    SS->>DB: 상태 변경 (REVIEWING)
    SS->>SS: ReportHistory 자동 생성
    SS->>DB: History 저장

    ADMIN->>SS: 제재 승인 요청
    SS->>DB: 상태 변경 (APPROVED)
    SS->>SS: Sanction 생성
    SS->>DB: 제재 저장
    SS-->>ADMIN: 처리 완료
Loading

2.4 1:1 문의 처리 흐름

sequenceDiagram
    participant U as User
    participant SS as Support Server
    participant DB as MariaDB
    participant ADMIN as Admin

    U->>SS: POST /api/v1/inquiries
    SS->>SS: 입력값 검증
    SS->>SS: 파일 첨부 검증 (최대 5개)
    SS->>DB: Inquiry 저장 (UNANSWERED)
    SS-->>U: InquiryResponse

    U->>SS: GET /api/v1/inquiries/{id}
    SS->>DB: 문의 조회
    SS-->>U: 문의 상세 정보

    ADMIN->>SS: POST /api/v1/inquiries/answers
    SS->>DB: Answer 저장
    SS->>DB: Inquiry 상태 변경 (ANSWERED)
    SS-->>ADMIN: AnswerResponse

    U->>SS: PATCH /api/v1/inquiries/{id}/confirm
    SS->>DB: 상태 변경 (CONFIRMED)
    SS-->>U: 확인 완료
Loading

2.5 FAQ 캐싱 흐름

flowchart TB
    START[서버 시작] --> INIT["@PostConstruct<br/>init()"]
    INIT --> LOAD[DB에서 FAQ 로드]
    LOAD --> CACHE[In-Memory Cache 저장]

    REQ[조회 요청] --> LOCK[ReadLock 획득]
    LOCK --> READ[캐시에서 조회]
    READ --> FILTER{카테고리 필터?}
    FILTER -->|ALL| ALL[전체 반환]
    FILTER -->|특정| FILTERED[필터링 후 반환]
    ALL --> UNLOCK[ReadLock 해제]
    FILTERED --> UNLOCK
    UNLOCK --> RES[응답]

    SCHEDULE["매일 03:00<br/>@Scheduled"] --> WLOCK[WriteLock 획득]
    WLOCK --> REFRESH[DB에서 재로드]
    REFRESH --> UPDATE[캐시 갱신]
    UPDATE --> WUNLOCK[WriteLock 해제]
Loading

3. 데이터 모델

3.1 ERD

erDiagram
    REPORT ||--o{ REPORT_HISTORY : has
    REPORT ||--o| SANCTION : triggers
    REPORT }o--|| REPORT_CATEGORY : references
    INQUIRY ||--o| ANSWER : has
    INQUIRY ||--o{ INQUIRY_FILE : contains

    REPORT {
        varchar report_id PK "Snowflake ID"
        varchar reporter_id "신고자 ID"
        varchar reported_id "피신고자 ID"
        enum reference_type "PROFILE, POST, BUSINESS"
        varchar report_category "신고 카테고리"
        varchar reason "신고 사유"
        enum status "PENDING, REVIEWING, etc."
        datetime reported_at "신고 일시"
    }

    REPORT_CATEGORY {
        enum reference_type PK "신고 대상 타입"
        varchar report_category PK "카테고리명"
    }

    REPORT_HISTORY {
        varchar history_id PK "Snowflake ID"
        varchar report_id FK "연관 신고"
        varchar admin_id "처리 관리자"
        enum previous_status "이전 상태"
        enum new_status "변경 상태"
        enum action_type "액션 타입"
        text comment "처리 코멘트"
        datetime created_at "생성 일시"
    }

    SANCTION {
        varchar sanction_id PK "Snowflake ID"
        varchar report_id FK "연관 신고"
        varchar target_id "제재 대상"
        enum sanction_type "WARNING, SUSPENSION, etc."
        int duration "제재 기간(일)"
        varchar reason "제재 사유"
        datetime sanctioned_at "제재 시작"
        datetime expires_at "제재 만료"
        enum status "ACTIVE, EXPIRED, REVOKED"
    }

    SANCTION_RULE {
        varchar rule_id PK "규칙 ID"
        enum reference_type "대상 타입"
        varchar category "카테고리"
        int threshold "임계값"
        enum sanction_type "제재 타입"
        int duration "기간"
    }

    REPORT_STATISTICS {
        varchar statistics_id PK
        varchar target_id "대상 ID"
        enum reference_type "대상 타입"
        int total_count "총 신고 수"
        int approved_count "승인된 수"
        datetime last_updated "마지막 갱신"
    }

    INQUIRY {
        varchar inquiry_id PK "UUID"
        varchar title "제목"
        text contents "내용"
        enum category "카테고리"
        enum status "UNANSWERED, ANSWERED, etc."
        varchar writer_id "작성자 ID"
        datetime created_at "생성 일시"
        datetime answered_at "답변 일시"
    }

    ANSWER {
        varchar answer_id PK "UUID"
        varchar inquiry_id FK "연관 문의"
        varchar writer_id "답변자 ID"
        text contents "답변 내용"
        datetime created_at "생성 일시"
    }

    INQUIRY_FILE {
        varchar inquiry_id PK,FK "문의 ID"
        int order_num PK "순서 (0-4)"
        varchar file_url "파일 URL"
    }

    FAQ {
        bigint id PK "자동 증가"
        enum category "카테고리"
        varchar title "제목"
        varchar question "질문"
        text answer "답변"
    }
Loading

3.2 테이블 상세

Report (신고)

필드 타입 필수 설명
report_id VARCHAR(100) Y Snowflake ID
reporter_id VARCHAR(100) Y 신고자 ID
reported_id VARCHAR(100) Y 피신고자/대상 ID
reference_type ENUM Y PROFILE, POST, BUSINESS
report_category VARCHAR(100) Y 신고 카테고리
reason VARCHAR(100) Y 신고 사유
status ENUM Y PENDING, REVIEWING, APPROVED, REJECTED, WITHDRAWN
reported_at DATETIME(6) Y 신고 일시

유니크 제약조건:

  • uk_report_per_user: (reporter_id, reference_type, reported_id, status)
    • 동일 사용자가 같은 대상에 대해 중복 신고 방지

ReportCategory (신고 카테고리)

필드 타입 필수 설명
reference_type ENUM Y PK, 신고 대상 타입
report_category VARCHAR(100) Y PK, 카테고리명

초기 데이터:

reference_type report_category 설명
PROFILE ABUSE 욕설/음란
PROFILE INAPPROPRIATE_INFO 부적절한 닉네임/사진
PROFILE ETC 기타
POST ABUSE 욕설/음란
POST SPAM 도배/스팸
POST DISPUTE 분란조장
POST ADVERTISEMENT 타업체광고
POST FRAUD 사기성
POST ETC 기타
BUSINESS FRAUD 사기성
BUSINESS CLOSED 폐업
BUSINESS ETC 기타

ReportHistory (신고 이력)

필드 타입 필수 설명
history_id VARCHAR(100) Y Snowflake ID
report_id VARCHAR(100) Y FK to Report
admin_id VARCHAR(100) N 처리 관리자 ID
previous_status ENUM Y 이전 상태
new_status ENUM Y 변경 후 상태
action_type ENUM Y STATUS_CHANGE 등
comment TEXT N 처리 코멘트
created_at DATETIME(6) Y 생성 일시

Sanction (제재)

필드 타입 필수 설명
sanction_id VARCHAR(100) Y Snowflake ID
report_id VARCHAR(100) Y FK to Report
target_id VARCHAR(100) Y 제재 대상 ID
sanction_type ENUM Y WARNING, SUSPENSION, PERMANENT_BAN
duration INT N 제재 기간 (일 단위, 영구정지 시 NULL)
reason VARCHAR(500) Y 제재 사유
sanctioned_at DATETIME(6) Y 제재 시작 일시
expires_at DATETIME(6) N 제재 만료 일시 (영구정지 시 NULL)
status ENUM Y ACTIVE, EXPIRED, REVOKED

Inquiry (문의)

필드 타입 필수 설명
inquiry_id VARCHAR(36) Y UUID
title VARCHAR(200) Y 문의 제목
contents VARCHAR(2000) Y 문의 내용
category ENUM Y RESERVATION, CHECK_IN, PAYMENT, REVIEW_REPORT, ETC
status ENUM Y UNANSWERED, ANSWERED, CONFIRMED
writer_id VARCHAR(64) Y 작성자 ID
created_at DATETIME(6) Y 생성 일시
updated_at DATETIME(6) Y 수정 일시
answered_at DATETIME(6) N 답변 일시

Answer (답변)

필드 타입 필수 설명
answer_id VARCHAR(36) Y UUID
inquiry_id VARCHAR(36) Y FK to Inquiry
writer_id VARCHAR(64) Y 답변자 ID (관리자)
contents TEXT Y 답변 내용
created_at DATETIME(6) Y 생성 일시

InquiryFile (문의 첨부파일)

필드 타입 필수 설명
inquiry_id VARCHAR(36) Y PK, FK to Inquiry
order_num INT Y PK, 순서 (0-4)
file_url VARCHAR(500) Y 파일 URL

제약조건: 문의당 최대 5개 파일 첨부 가능

FAQ (자주 묻는 질문)

필드 타입 필수 설명
id BIGINT Y 자동 증가 PK
category ENUM Y ALL, RESERVATION, CHECK_IN, PAYMENT, REVIEW_REPORT, ETC
title VARCHAR(200) Y 제목
question VARCHAR(500) Y 질문
answer TEXT Y 답변

4. API 명세

4.1 신고 API

신고 등록

POST /api/v1/reports

Request

필드 타입 필수 설명
reporterId String Y 신고자 ID
reportedId String Y 피신고자/대상 ID
referenceType String Y PROFILE, POST, BUSINESS
reportCategory String Y 신고 카테고리
reason String Y 신고 사유 (최대 500자)

Request Example

{
  "reporterId": "1234567890123456789",
  "reportedId": "9876543210987654321",
  "referenceType": "PROFILE",
  "reportCategory": "ABUSE",
  "reason": "욕설이 포함된 프로필입니다."
}

Response

{
  "reportId": "5555555555555555555",
  "reporterId": "1234567890123456789",
  "reportedId": "9876543210987654321",
  "referenceType": "PROFILE",
  "reportCategory": "ABUSE",
  "reason": "욕설이 포함된 프로필입니다.",
  "status": "PENDING",
  "reportedAt": "2025-01-20T14:30:00"
}

상태 코드

코드 설명
201 신고 등록 성공
400 잘못된 요청 형식
404 카테고리를 찾을 수 없음
409 중복 신고

신고 상세 조회

GET /api/v1/reports/{reportId}

Response

{
  "reportId": "5555555555555555555",
  "reporterId": "1234567890123456789",
  "reportedId": "9876543210987654321",
  "referenceType": "PROFILE",
  "reportCategory": "ABUSE",
  "reason": "욕설이 포함된 프로필입니다.",
  "status": "REVIEWING",
  "reportedAt": "2025-01-20T14:30:00",
  "histories": [
    {
      "historyId": "6666666666666666666",
      "adminId": "admin123",
      "previousStatus": "PENDING",
      "newStatus": "REVIEWING",
      "actionType": "STATUS_CHANGE",
      "comment": "검토 시작",
      "createdAt": "2025-01-20T15:00:00"
    }
  ]
}

신고 목록 검색 (커서 페이징)

GET /api/v1/reports

Query Parameters

파라미터 타입 필수 기본값 설명
status String N - 상태 필터
referenceType String N - 대상 타입 필터
reportCategory String N - 카테고리 필터
sortType String N REPORTED_AT 정렬 기준
sortDirection String N DESC 정렬 방향
cursor String N - 커서 (이전 페이지 마지막 ID)
size Int N 20 페이지 크기

Response

{
  "content": [
    {
      "reportId": "5555555555555555555",
      "reporterId": "1234567890123456789",
      "reportedId": "9876543210987654321",
      "referenceType": "PROFILE",
      "status": "PENDING",
      "reportedAt": "2025-01-20T14:30:00"
    }
  ],
  "cursor": "5555555555555555555",
  "hasNext": true,
  "size": 20
}

신고 상태 변경

PATCH /api/v1/reports/{reportId}

Request

필드 타입 필수 설명
status String Y REVIEWING, APPROVED, REJECTED, PENDING
adminId String Y 처리 관리자 ID
comment String N 처리 코멘트

Request Example

{
  "status": "APPROVED",
  "adminId": "admin123",
  "comment": "신고 내용 확인 완료, 제재 적용"
}

신고 철회

DELETE /api/v1/reports/{reportId}

Request

{
  "reporterId": "1234567890123456789",
  "reason": "오해였습니다."
}

설명

  • 신고자 본인만 철회 가능
  • PENDING 상태의 신고만 철회 가능

4.2 문의 API

문의 등록

POST /api/v1/inquiries

Request

필드 타입 필수 설명
title String Y 제목 (최대 200자)
contents String Y 내용 (최대 2000자)
category String Y 카테고리
writerId String Y 작성자 ID
files List N 첨부파일 URL (최대 5개)

Request Example

{
  "title": "예약 취소 문의",
  "contents": "예약을 취소하고 싶습니다. 환불 절차가 어떻게 되나요?",
  "category": "RESERVATION",
  "writerId": "1234567890123456789",
  "files": [
    "https://storage.example.com/images/receipt.jpg"
  ]
}

Response

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "title": "예약 취소 문의",
  "contents": "예약을 취소하고 싶습니다. 환불 절차가 어떻게 되나요?",
  "category": "RESERVATION",
  "status": "UNANSWERED",
  "writerId": "1234567890123456789",
  "createdAt": "2025-01-20T14:30:00",
  "files": [
    "https://storage.example.com/images/receipt.jpg"
  ]
}

문의 상세 조회

GET /api/v1/inquiries/{inquiryId}

Response

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "title": "예약 취소 문의",
  "contents": "예약을 취소하고 싶습니다.",
  "category": "RESERVATION",
  "status": "ANSWERED",
  "writerId": "1234567890123456789",
  "createdAt": "2025-01-20T14:30:00",
  "answeredAt": "2025-01-20T15:30:00",
  "answer": {
    "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "contents": "예약 취소는 마이페이지에서 가능합니다.",
    "writerId": "admin123",
    "createdAt": "2025-01-20T15:30:00"
  }
}

문의 목록 조회

GET /api/v1/inquiries

Query Parameters

파라미터 타입 필수 설명
writerId String N 작성자 ID 필터
category String N 카테고리 필터
status String N 상태 필터

문의 삭제

DELETE /api/v1/inquiries/{inquiryId}?writerId={writerId}

설명

  • 작성자 본인만 삭제 가능

답변 등록

POST /api/v1/inquiries/answers

Request

{
  "inquiryId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "contents": "예약 취소는 마이페이지에서 가능합니다.",
  "writerId": "admin123"
}

답변 삭제

DELETE /api/v1/inquiries/{inquiryId}/answer

답변 확인

PATCH /api/v1/inquiries/{inquiryId}/confirm?writerId={writerId}

설명

  • 문의 작성자가 답변을 확인했음을 표시
  • 상태가 CONFIRMED로 변경됨

4.3 FAQ API

FAQ 목록 조회

GET /api/v1/faqs

Query Parameters

파라미터 타입 필수 기본값 설명
category String N ALL ALL, RESERVATION, CHECK_IN, PAYMENT, REVIEW_REPORT, ETC

Response

[
  {
    "id": 1,
    "category": "RESERVATION",
    "title": "예약 변경",
    "question": "예약을 변경하려면 어떻게 해야 하나요?",
    "answer": "마이페이지 > 예약 내역에서 변경하실 수 있습니다."
  },
  {
    "id": 2,
    "category": "PAYMENT",
    "title": "환불 정책",
    "question": "환불은 언제 처리되나요?",
    "answer": "취소 후 영업일 기준 3-5일 내에 환불됩니다."
  }
]

FAQ 캐시 수동 갱신

POST /api/v1/faqs/refresh

Response

FAQ cache refreshed successfully

4.4 헬스 체크

GET /health

Response

Server is up

5. 비즈니스 규칙

5.1 신고 상태 전이

stateDiagram-v2
    [*] --> PENDING: 신고 접수
    PENDING --> REVIEWING: 검토 시작
    PENDING --> WITHDRAWN: 신고자 철회
    REVIEWING --> APPROVED: 승인 (제재 적용)
    REVIEWING --> REJECTED: 기각
    REVIEWING --> PENDING: 보류 처리
    APPROVED --> [*]
    REJECTED --> [*]
    WITHDRAWN --> [*]
Loading

5.2 문의 상태 전이

stateDiagram-v2
    [*] --> UNANSWERED: 문의 등록
    UNANSWERED --> ANSWERED: 답변 등록
    ANSWERED --> UNANSWERED: 답변 삭제
    ANSWERED --> CONFIRMED: 사용자 확인
    CONFIRMED --> [*]
Loading

5.3 제재 유형

유형 설명 기간
WARNING 경고 즉시 (기록만)
SUSPENSION 일시 정지 1~30일
PERMANENT_BAN 영구 정지 무기한

5.4 제재 상태

상태 설명
ACTIVE 활성 (적용 중)
EXPIRED 만료됨
REVOKED 취소됨 (관리자 해제)

5.5 신고 규칙

규칙 설명
중복 신고 방지 동일 사용자가 같은 대상에 동일 상태로 중복 신고 불가
철회 제한 PENDING 상태의 신고만 철회 가능
철회 권한 신고자 본인만 철회 가능
히스토리 자동화 상태 변경 시 자동으로 이력 기록

5.6 문의 규칙

규칙 설명
파일 첨부 제한 문의당 최대 5개 파일
답변 중복 방지 문의당 1개 답변만 가능
삭제 권한 작성자 본인만 삭제 가능
확인 제한 ANSWERED 상태만 확인 가능

6. 캐싱 전략

6.1 FAQ 인메모리 캐시

flowchart LR
    subgraph Application
        FAQ_SVC[FaqService]
        CACHE[In-Memory Cache<br/>volatile List]
        LOCK[ReadWriteLock]
    end

    subgraph Database
        DB[(MariaDB)]
    end

    FAQ_SVC -->|읽기| CACHE
    FAQ_SVC -->|쓰기| LOCK
    LOCK -->|갱신| CACHE
    CACHE -.->|초기화/갱신| DB
Loading

6.2 캐시 동작

작업 시점 동작
초기화 서버 시작 (@PostConstruct) DB 전체 로드
자동 갱신 매일 03:00 (@Scheduled) DB 재로드
수동 갱신 API 호출 DB 재로드
읽기 조회 요청 캐시에서 조회

6.3 동시성 제어

// 읽기: ReadLock (동시 접근 허용)
lock.readLock().lock();
try {
    return new ArrayList<>(cachedFaqs);
} finally {
    lock.readLock().unlock();
}

// 쓰기: WriteLock (단독 접근)
lock.writeLock().lock();
try {
    cachedFaqs = faqRepository.findAll();
} finally {
    lock.writeLock().unlock();
}

6.4 스케일 아웃 고려사항

항목 설명
독립 캐시 각 인스턴스가 독립적인 로컬 캐시 유지
분산 락 불필요 읽기 전용이므로 분산 락 불필요
일관성 스케줄러로 주기적 갱신하여 최종 일관성 보장

6.5 신고 카테고리 캐시

// ReportCategoryCache 인터페이스
public interface ReportCategoryCache {
    Optional<ReportCategory> get(ReferenceType type, String category);
    void refresh();
}

// InMemoryReportCategoryCache 구현
// - 서버 시작 시 초기화
// - 신고 등록 시 카테고리 유효성 검증에 활용

7. 인덱스 설계

7.1 Report 테이블

-- 신고자별 조회
CREATE INDEX idx_report_reporter_id ON report (reporter_id);

-- 피신고자별 조회
CREATE INDEX idx_report_reported_id ON report (reported_id);

-- 신고 일시 정렬
CREATE INDEX idx_report_reported_at ON report (reported_at);

-- 상태별 필터링
CREATE INDEX idx_report_status ON report (status);

-- 중복 신고 방지 (유니크)
CREATE UNIQUE INDEX uk_report_per_user
    ON report (reporter_id, reference_type, reported_id, status);

7.2 Sanction 테이블

-- 대상별 제재 조회
CREATE INDEX idx_sanctions_target_id ON sanctions (target_id);

-- 상태별 필터링
CREATE INDEX idx_sanctions_status ON sanctions (status);

-- 만료 스케줄러 조회
CREATE INDEX idx_sanctions_expires_at ON sanctions (expires_at);

7.3 Inquiry 테이블

-- 작성자별 조회
CREATE INDEX idx_inquiry_writer_id ON inquiries (writer_id);

-- 카테고리별 조회
CREATE INDEX idx_inquiry_category ON inquiries (category);

-- 상태별 필터링
CREATE INDEX idx_inquiry_status ON inquiries (status);

8. 에러 코드

8.1 신고 에러

코드 HTTP Status 설명
REPORT_NOT_FOUND 404 신고를 찾을 수 없음
REPORT_CATEGORY_NOT_FOUND 404 카테고리를 찾을 수 없음

8.2 문의 에러

코드 HTTP Status 설명
INQUIRY_NOT_FOUND 404 문의를 찾을 수 없음
ANSWER_NOT_FOUND 404 답변을 찾을 수 없음
ANSWER_ALREADY_EXISTS 409 이미 답변이 존재함
UNAUTHORIZED_ACCESS 403 접근 권한 없음
INVALID_INQUIRY_STATUS 400 잘못된 문의 상태

8.3 공통 에러

코드 HTTP Status 설명
VALIDATION_ERROR 400 입력값 검증 실패
INTERNAL_ERROR 500 서버 내부 오류

9. 환경 설정

9.1 환경 변수

# Database
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_NAME=support_db
DATABASE_USER_NAME=support
DATABASE_PASSWORD=your_password

# Redis (선택)
REDIS_HOST=localhost
REDIS_PORT=6379

# Snowflake ID
SNOWFLAKE_DATACENTER_ID=1
SNOWFLAKE_WORKER_ID=1

# Spring Profile
SPRING_PROFILES_ACTIVE=dev

9.2 application.yaml

spring:
  profiles:
    active: dev

---
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:mariadb://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}
    username: ${DATABASE_USER_NAME}
    password: ${DATABASE_PASSWORD}
    driver-class-name: org.mariadb.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: true
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}

9.3 Docker 배포

Dockerfile

FROM eclipse-temurin:21-jdk AS build
WORKDIR /app

COPY SupportServer/gradlew .
COPY SupportServer/gradle ./gradle
COPY SupportServer/build.gradle settings.gradle ./
COPY SupportServer/src ./src

RUN chmod +x ./gradlew && ./gradlew clean bootJar

FROM eclipse-temurin:21-jre
WORKDIR /app

COPY --from=build /app/build/libs/*.jar /app/app.jar

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

멀티 아키텍처 빌드

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ddingsh9/support-server:1.0.0.0 \
  --push .

9.4 Docker Compose

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    container_name: nginx-support
    ports:
      - "9100:80"
    depends_on:
      - support-server-1
      - support-server-2
      - support-server-3

  support-server-1:
    image: ddingsh9/support-server:1.0.0.0
    env_file:
      - .env.prod
    container_name: support-server-1
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 15s
      timeout: 5s
      retries: 3

  support-server-2:
    image: ddingsh9/support-server:1.0.0.0
    env_file:
      - .env.prod
    container_name: support-server-2
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 15s
      timeout: 5s
      retries: 3

  support-server-3:
    image: ddingsh9/support-server:1.0.0.0
    env_file:
      - .env.prod
    container_name: support-server-3
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 15s
      timeout: 5s
      retries: 3

networks:
  support-network:
    driver: bridge
  infra-network:
    external: true

9.5 Nginx 설정

upstream support_upstream {
    server support-server-1:8080;
    server support-server-2:8080;
    server support-server-3:8080;
}

server {
    listen 80;

    location / {
        proxy_pass http://support_upstream;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

10. 스케줄링

10.1 스케줄 작업

작업 크론 표현식 설명
FAQ 캐시 갱신 0 0 3 * * * 매일 새벽 3시 갱신
제재 만료 처리 0 0 0 * * * 매일 자정 만료 상태 변경

10.2 스케줄러 구현

@Scheduled(cron = "0 0 3 * * *")
public void scheduledRefreshCache() {
    log.info("Scheduled FAQ cache refresh started");
    refreshCache();
    log.info("Scheduled FAQ cache refresh completed");
}

11. 패키지 구조

com.teambind.supportserver/
├── common/
│   ├── config/
│   │   ├── IdConfig.java
│   │   └── QueryDslConfig.java
│   ├── controller/
│   │   └── HealthController.java
│   ├── handler/
│   │   └── GlobalExceptionHandler.java
│   └── utils/
│       ├── IdGenerator.java
│       └── Snowflake.java
├── report/
│   ├── aop/
│   │   └── PerformanceMonitoringAspect.java
│   ├── controller/
│   │   ├── ReportController.java
│   │   └── CacheManagementController.java
│   ├── service/
│   │   ├── ReportService.java
│   │   ├── ReportServiceImpl.java
│   │   ├── ReportStatisticsService.java
│   │   └── SanctionService.java
│   ├── repository/
│   │   ├── ReportRepository.java
│   │   ├── ReportRepositoryCustom.java
│   │   ├── ReportRepositoryImpl.java
│   │   ├── ReportCategoryRepository.java
│   │   ├── ReportHistoryRepository.java
│   │   ├── ReportStatisticsRepository.java
│   │   ├── SanctionRepository.java
│   │   └── SanctionRuleRepository.java
│   ├── entity/
│   │   ├── Report.java
│   │   ├── ReportCategory.java
│   │   ├── ReportHistory.java
│   │   ├── ReportStatistics.java
│   │   ├── Sanction.java
│   │   ├── SanctionRule.java
│   │   ├── embeddable/
│   │   │   └── ReportCategoryId.java
│   │   └── enums/
│   │       ├── ActionType.java
│   │       ├── ReferenceType.java
│   │       ├── ReportStatus.java
│   │       ├── SanctionStatus.java
│   │       └── SanctionType.java
│   ├── dto/
│   │   ├── request/
│   │   │   ├── ReportRequest.java
│   │   │   ├── ReportSearchRequest.java
│   │   │   └── ReportStatusUpdateRequest.java
│   │   └── response/
│   │       ├── ReportResponse.java
│   │       └── CursorPageResponse.java
│   ├── exceptions/
│   │   ├── ErrorCode.java
│   │   └── ReportException.java
│   └── utils/
│       ├── ReportCategoryCache.java
│       └── InMemoryReportCategoryCache.java
├── inquiries/
│   ├── controller/
│   │   └── InquiryController.java
│   ├── service/
│   │   ├── InquiryService.java
│   │   └── InquiryServiceImpl.java
│   ├── repository/
│   │   ├── InquiryRepository.java
│   │   └── AnswerRepository.java
│   ├── entity/
│   │   ├── Inquiry.java
│   │   ├── Answer.java
│   │   ├── InquiryFile.java
│   │   ├── InquiryCategory.java
│   │   └── InquiryStatus.java
│   ├── dto/
│   │   ├── request/
│   │   │   ├── InquiryCreateRequest.java
│   │   │   └── AnswerCreateRequest.java
│   │   └── response/
│   │       ├── InquiryResponse.java
│   │       └── AnswerResponse.java
│   └── exceptions/
│       ├── ErrorCode.java
│       └── InquiryException.java
└── faq/
    ├── controller/
    │   └── FaqController.java
    ├── service/
    │   └── FaqService.java
    ├── repository/
    │   └── FaqRepository.java
    └── entity/
        ├── Faq.java
        └── enums/
            └── FaqCategory.java

12. 구현 우선순위

Phase 1 - 핵심 기능 (완료)

  • 신고 등록/조회/검색 API
  • 신고 상태 변경
  • 신고 히스토리 자동 기록
  • 커서 기반 페이징
  • Snowflake ID 생성

Phase 2 - 문의 기능 (완료)

  • 문의 CRUD
  • 답변 등록/삭제
  • 답변 확인 기능
  • 파일 첨부 지원

Phase 3 - FAQ (완료)

  • FAQ 조회 API
  • 인메모리 캐싱
  • 스케줄 기반 캐시 갱신
  • 카테고리 필터링

Phase 4 - 제재 시스템 (완료)

  • 제재 엔티티 설계
  • 제재 규칙 엔티티
  • 신고 통계 엔티티

Phase 5 - 고도화 (진행 중)

  • Kafka 이벤트 연동
  • 제재 자동화 로직
  • 관리자 대시보드 API
  • Prometheus + Grafana 모니터링

13. 테스트

13.1 테스트 커버리지

./gradlew test jacocoTestReport

JaCoCo 설정:

  • 제외 대상: config, entity, dto, exceptions, Application

13.2 테스트 현황

도메인 테스트 파일 수 주요 테스트
Report 15+ 엔티티, 리포지토리, 서비스, 컨트롤러
Inquiry 5+ 엔티티, 리포지토리
FAQ 2+ 서비스, 컨트롤러

14. 참고 사항

14.1 Snowflake ID 생성

Support Server는 분산 환경에서 고유 ID를 생성하기 위해 Snowflake 알고리즘을 사용한다.

|-- 1 bit --|-- 41 bits --|-- 10 bits --|-- 12 bits --|
|   sign    |  timestamp  |  machine id | sequence    |
  • 64-bit 고유 ID: 충돌 없는 분산 ID 생성
  • 시간순 정렬: timestamp 기반으로 자연 정렬 가능
  • 초당 4096개: 동일 밀리초 내 최대 시퀀스

14.2 QueryDSL 활용

// 동적 검색 조건
BooleanBuilder builder = new BooleanBuilder();

if (request.getStatus() != null) {
    builder.and(report.status.eq(request.getStatus()));
}

if (request.getReferenceType() != null) {
    builder.and(report.referenceType.eq(request.getReferenceType()));
}

// 커서 기반 페이징
if (cursor != null) {
    builder.and(report.reportId.lt(cursor));
}

14.3 비즈니스 로직 위치

로직 유형 위치 예시
상태 전이 Entity Report.changeStatus()
연관관계 관리 Entity Inquiry.addAnswer()
검증 로직 Entity validateFiles()
복잡한 조회 Repository ReportRepositoryImpl
트랜잭션 조율 Service ReportServiceImpl

버전: 0.0.1-SNAPSHOT 최종 업데이트: 2025-01-20 : TeamBiund Development Team

About

1대1 문의 , FAQ, 신고 관련 서비

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published