Vidding 데이터 모델 명세
새 Supabase 프로젝트(
vidding-re)에 처음부터 구축하는 스키마다. 기존 서비스의 스키마를 이어받지 않으며, 데이터 이관도 없다. 기준 문서: PRD · 기능 스펙
1. 설계 원칙
| # | 원칙 | 이유 |
|---|---|---|
| P1 | 사용자 유형 컬럼을 만들지 않는다 | 권한은 관계로 판정한다 (00-관계-판정) |
| P2 | 관계는 저장하지 않고 도출한다 | 주최자·참여자는 기존 컬럼으로 계산된다. 별도 필드를 두지 않는다 |
| P3 | 다대다 관계는 조인 테이블로 만든다 | 배열 컬럼은 동시 수정 시 서로 덮어쓴다 (§7.1) |
| P4 | 원장은 append-only로 쌓는다 | 포인트 이력을 수정·삭제하지 않는다 |
| P5 | 파생 상태는 저장하지 않는다 | '마감 임박'은 마감 시각으로 계산한다 |
| P6 | 권한은 RLS로 강제한다 | 클라이언트가 아니라 DB가 최종 판정한다 (§6) |
2. 전체 구조
users ─┬─< addresses
├─< auctions ─┬─< episodes ──< episode_likes
│ ├─< auction_favorites
│ └─── chat_rooms ──< messages
├─< points
└─< notifications
| 테이블 | 역할 |
|---|---|
users | 계정 |
addresses | 배송지 |
auctions | 경매 |
episodes | 사연 (입찰) |
episode_likes | 공감 |
auction_favorites | 찜 |
points | 포인트 원장 |
chat_rooms | 채팅방 |
messages | 메시지 |
notifications | 알림 |
3. 테이블 정의
3.1 users
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id | uuid | PK, FK → auth.users.id | 인증 사용자 ID |
email | text | NOT NULL, UNIQUE | |
nick_name | text | NOT NULL | |
avatar_url | text | NULL | 소셜 프로필 이미지 |
point_balance | integer | NOT NULL, DEFAULT 0, CHECK ≥ 0 | 현재 보유 포인트 |
created_at | timestamptz | NOT NULL, DEFAULT now() | |
updated_at | timestamptz | NULL |
role컬럼이 없다. 이 스키마에는 사용자 유형 개념이 존재하지 않는다 (P1).
point_balance가 잔액의 단일 출처다.points테이블은 이력이며, 잔액을 합산해 구하지 않는다CHECK ≥ 0으로 잔액이 음수가 되는 것을 DB에서 막는다
3.2 addresses
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id | uuid | PK | |
user_id | uuid | NOT NULL, FK → users.id ON DELETE CASCADE | |
recipient | text | NOT NULL | 받는 사람 |
phone | text | NOT NULL | 연락처 |
zipcode | text | NOT NULL | 우편번호 (주소 검색으로만 입력) |
address1 | text | NOT NULL | 기본 주소 (주소 검색으로만 입력) |
address2 | text | NULL | 상세 주소 |
created_at | timestamptz | NOT NULL, DEFAULT now() |
제약 — UNIQUE (user_id) : 사용자당 배송지 1개
목록 화면도 기본 배송지 개념도 없다. 하나뿐이므로 구분할 대상이 없다 (F12 3.3).
3.3 auctions
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id | uuid | PK | |
user_id | uuid | NOT NULL, FK → users.id | 주최자 |
address_id | uuid | NOT NULL, FK → addresses.id | 발송지 스냅샷 참조 |
title | text | NOT NULL | |
description | text | NOT NULL | 사연 요청 설명 |
image_urls | text[] | NOT NULL, CHECK 길이 1~3 | 이미지 (순서 있는 값 목록) |
end_at | timestamptz | NOT NULL | 마감 시각 |
status | text | NOT NULL, DEFAULT 'OPEN', CHECK IN ('OPEN','CLOSED') | |
winning_episode_id | uuid | NULL, FK → episodes.id | 낙찰 사연 |
closed_at | timestamptz | NULL | 마감 처리 완료 시각 |
created_at | timestamptz | NOT NULL, DEFAULT now() | |
updated_at | timestamptz | NULL |
상태를 2개만 둔다 — OPEN / CLOSED. '마감 임박'은 end_at으로 계산하는 파생 상태이므로 저장하지 않는다 (P5).
입찰 포인트 컬럼이 없다. 입찰 단계는 전 서비스 공통 상수이므로 경매마다 저장하지 않는다.
1,000 → 1,500 → 2,000 → 2,500 → 3,000 (+500 단위, 상한 3,000)
주최자가 시작·최대 포인트를 정하지 않는다. 입력 항목이 줄고, 검증 규칙이 사라지고, 교환 비율이 항상 명확해진다. 낙찰은 어차피 공감이 결정하므로 잃는 것이 없다.
image_urls만 배열을 유지한다. 순서가 의미를 갖고 동시 수정 주체가 주최자 한 명이라 P3의 문제가 발생하지 않는다.
3.4 episodes
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id | uuid | PK | |
auction_id | uuid | NOT NULL, FK → auctions.id ON DELETE CASCADE | |
user_id | uuid | NOT NULL, FK → users.id | 작성자 |
title | text | NOT NULL, CHECK 길이 2~50 | |
content | text | NOT NULL, CHECK 길이 5~1000 | |
bid_amount | integer | NOT NULL, DEFAULT 0, CHECK ≥ 0 | 본인이 건 포인트 누적액 |
created_at | timestamptz | NOT NULL, DEFAULT now() | 동점 판정 기준 |
updated_at | timestamptz | NULL |
제약
| 제약 | 내용 |
|---|---|
UNIQUE (auction_id, user_id) | 한 경매에 사연 1개 (F3) |
CHECK bid_amount IN (0, 1000, 1500, 2000, 2500, 3000) | 고정 입찰 단계만 허용 |
주최자 배제는 애플리케이션과 RLS에서 처리한다 (§6).
created_at은 낙찰 동점 기준이다 (F5 3.2.1). 수정해도 갱신하지 않는다.
3.5 episode_likes
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id | uuid | PK | |
episode_id | uuid | NOT NULL, FK → episodes.id ON DELETE CASCADE | |
user_id | uuid | NOT NULL, FK → users.id | 공감한 사람 |
weight | integer | NOT NULL, CHECK IN (10, 50) | 부여 시점의 가중치 |
created_at | timestamptz | NOT NULL, DEFAULT now() |
제약 — UNIQUE (episode_id, user_id) : 한 사람이 한 사연에 한 번만
weight를 저장하는 이유
| 값 | 조건 |
|---|---|
50 | 공감한 사람이 해당 경매의 주최자 |
10 | 그 외 |
가중치를 코드 상수로만 두면, 나중에 값이 바뀌었을 때 공감 해제 시 다른 값이 차감된다. 부여 시점 값을 함께 저장해 F4 완료 조건 3(적용된 가중치와 같은 값 차감) 을 구조적으로 보장한다.
3.6 auction_favorites
| 컬럼 | 타입 | 제약 |
|---|---|---|
id | uuid | PK |
auction_id | uuid | NOT NULL, FK → auctions.id ON DELETE CASCADE |
user_id | uuid | NOT NULL, FK → users.id |
created_at | timestamptz | NOT NULL, DEFAULT now() |
제약 — UNIQUE (auction_id, user_id)
찜은 개인 북마크다. 찜 수를 공개하지 않으며 정렬·순위에 쓰지 않는다 (F7). 인기순 정렬 기준은 모인 사연 수다.
3.7 points
포인트 원장(ledger) 이다. 한 번 쌓인 행은 수정·삭제하지 않는다 (P4).
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id | uuid | PK | |
user_id | uuid | NOT NULL, FK → users.id | |
amount | integer | NOT NULL, CHECK ≠ 0 | 적립 + / 차감 − |
balance_after | integer | NOT NULL | 처리 후 잔액 (F8 표시용) |
type | text | NOT NULL | 아래 표 참조 |
auction_id | uuid | NULL, FK → auctions.id | 관련 경매 |
episode_id | uuid | NULL, FK → episodes.id | 관련 사연 |
description | text | NOT NULL | 사용자에게 보일 사유 |
created_at | timestamptz | NOT NULL, DEFAULT now() |
type 값
| 값 | 부호 | 대상 | 발생 시점 |
|---|---|---|---|
SIGNUP_BONUS | + | 신규 가입자 | 가입 시 5,000 P 1회 지급 |
BID | − | 참여자 | 사연에 포인트를 걸 때 (F3) |
BID_REFUND_LOST | + | 미낙찰 참여자 | 마감 시 반환 (F5) |
BID_REFUND_VOID | + | 참여자 전원 | 유찰로 인한 반환 (F5) |
BID_REFUND_CANCEL | + | 참여자 | 마감 전 사연 삭제로 인한 반환 (F3) |
WIN_TRANSFER | + | 주최자 | 낙찰 확정 시 낙찰자가 건 포인트를 수취 (F5) |
낙찰자에게는 정산 행이 추가되지 않는다. 입찰 시점의
BID차감이 그대로 확정된다.
amount와 users.point_balance는 같은 트랜잭션에서 함께 갱신한다 (§4).
포인트 순환 구조
물건을 나누면 포인트를 얻고, 물건을 받으면 포인트를 쓴다.
[받는 쪽] 사연 낙찰 → 건 포인트가 차감 확정
↓ 이전
[주는 쪽] 경매 낙찰 → 낙찰자가 건 포인트를 수취
| 항목 | 내용 |
|---|---|
| 이전 금액 | 낙찰 사연의 bid_amount (본인이 직접 건 포인트) |
| 공감 가중치 | 이전 대상이 아니다. 실제 포인트가 아니라 점수이기 때문이다 (F4) |
| 유찰 | 이전 없음. 참여자 전원에게 반환한다 |
총량이 보존된다. 낙찰 순환에서는 포인트가 생기거나 사라지지 않고 이동만 한다. 신규 발행은 SIGNUP_BONUS 하나뿐이다.
자전거래가 구조적으로 불가능하다. 주최자는 자기 경매에 사연을 쓸 수 없으므로(F3), 자기 경매에서 자기에게 포인트를 보낼 수 없다.
3.8 chat_rooms
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id | uuid | PK | |
auction_id | uuid | NOT NULL, UNIQUE, FK → auctions.id | 경매당 채팅방 1개 |
created_at | timestamptz | NOT NULL, DEFAULT now() |
참여자 컬럼이 없다.
채팅은 낙찰 이후 주최자와 낙찰자 사이에서만 열린다 (F6). 두 사람 모두 경매에서 도출된다.
주최자 = auctions.user_id
낙찰자 = episodes.user_id (auctions.winning_episode_id 가 가리키는 사연)
기존 스키마의
seller_id/buyer_id에 해당하는 컬럼이 필요 없다. 참여자를 별도로 저장하면 경매 데이터와 어긋날 수 있으므로 도출한다 (P2).
채팅방은 낙찰 확정 시에만 생성한다. 유찰된 경매에는 만들지 않는다.
3.9 messages
| 컬럼 | 타입 | 제약 |
|---|---|---|
id | uuid | PK |
chat_room_id | uuid | NOT NULL, FK → chat_rooms.id ON DELETE CASCADE |
sender_id | uuid | NOT NULL, FK → users.id |
content | text | NOT NULL, CHECK 공백 제외 길이 ≥ 1 |
read_at | timestamptz | NULL |
created_at | timestamptz | NOT NULL, DEFAULT now() |
read_at이 NULL이면 읽지 않은 메시지다. 별도 플래그를 두지 않는다.
3.10 notifications
| 컬럼 | 타입 | 제약 | 설명 |
|---|---|---|---|
id | uuid | PK | |
user_id | uuid | NOT NULL, FK → users.id ON DELETE CASCADE | 수신자 |
type | text | NOT NULL | 아래 표 참조 |
title | text | NOT NULL | |
body | text | NOT NULL | |
auction_id | uuid | NULL, FK → auctions.id | 이동 대상 |
chat_room_id | uuid | NULL, FK → chat_rooms.id | 이동 대상 |
read_at | timestamptz | NULL | |
created_at | timestamptz | NOT NULL, DEFAULT now() |
type 값
| 값 | 발생 |
|---|---|
AUCTION_ENDING_SOON_3D | 마감 3일 전 (§11.3) |
AUCTION_ENDING_SOON_1D | 마감 1일 전 |
AUCTION_ENDING_SOON_1H | 마감 1시간 전 |
EPISODE_CREATED | 사연 등록 |
AUCTION_RESULT | 마감 처리 완료 |
CHAT_MESSAGE | 메시지 도착 |
마감 임박을 단계별 type으로 나눈 이유가 아래 인덱스다. 하나의 값으로 두면 (사용자, 종류, 경매)당 1건 제한에 걸려 단계마다 보낼 수 없다. AUCTION_ENDING_SOON_% 접두어로 임박 알림 여부를 판정한다 (F9 3.4 경고색).
중복 방지 — 같은 사건에 대한 중복 발송을 막기 위해 부분 유니크 인덱스를 둔다 (F9).
UNIQUE (user_id, type, auction_id)
WHERE type IN ('AUCTION_ENDING_SOON_3D','AUCTION_ENDING_SOON_1D',
'AUCTION_ENDING_SOON_1H','AUCTION_RESULT')
CHAT_MESSAGE는 반복 발생하므로 이 제약에서 제외한다.
단계마다 1건이다. 한 경매에 대해 한 사람이 같은 단계의 알림을 두 번 받지 않는다. 크론이 5분마다 돌아도 이 인덱스가 중복을 막으므로, 반복 실행이 곧 재시도가 된다.
푸시 알림을 만들지 않는다. 알림은 서비스 안에서만 확인한다 (F9).
4. 트랜잭션이 필요한 처리
다음 처리는 여러 테이블을 원자적으로 바꾼다. 각각 DB 함수(RPC)로 구현한다.
4.1 포인트 입찰 place_bid(episode_id, new_amount)
| 순서 | 처리 |
|---|---|
| 1 | 경매가 OPEN이고 마감 시각 이전인지 확인 |
| 2 | 요청자가 사연 작성자인지 확인 |
| 3 | new_amount가 고정 단계 값인지 확인 (1000 / 1500 / 2000 / 2500 / 3000) |
| 4 | new_amount > episodes.bid_amount 확인 — 올리기만 가능 |
| 5 | users.point_balance ≥ (new_amount − bid_amount) 확인 |
| 6 | episodes.bid_amount = new_amount |
| 7 | users.point_balance −= 차액 |
| 8 | points에 BID 행 삽입 (차액만큼) |
하나라도 실패하면 전부 롤백한다.
4.2 마감 처리 close_auction(auction_id)
| 순서 | 처리 |
|---|---|
| 1 | status = 'CLOSED', closed_at 기록 |
| 2 | 최종 점수 1위 사연을 winning_episode_id에 기록 (동점 시 created_at 빠른 쪽) |
| 3 | 미낙찰 참여자에게 BID_REFUND_LOST 반환 + point_balance 증가 |
| 4 | 주최자에게 WIN_TRANSFER 지급 — 낙찰 사연의 bid_amount 만큼 + point_balance 증가 |
| 5 | 사연이 0건이면 유찰 — winning_episode_id는 NULL, 참여자 전원 BID_REFUND_VOID 반환, 4번 생략 |
| 6 | 낙찰이 있으면 chat_rooms 생성 |
| 7 | notifications 삽입 |
1~6은 한 트랜잭션이다. 마감됐는데 낙찰자가 없는 중간 상태를 만들지 않는다 (F5). 7(알림)은 실패해도 롤백하지 않는다 (F5·F9).
포인트 총량 검증 — 3번(반환)과 4번(이전)의 합이 해당 경매의
BID차감 총액과 정확히 일치해야 한다. 트랜잭션 종료 시 이 등식이 깨지면 롤백한다.
4.3 사연 삭제 delete_episode(episode_id)
| 조건 | 처리 |
|---|---|
경매가 OPEN | BID_REFUND_CANCEL로 bid_amount 전액 반환 후 삭제 |
경매가 CLOSED + 미낙찰 | 반환 없이 삭제 (마감 시 이미 반환됨) |
경매가 CLOSED + 낙찰 | 거부 (F3 3.4) |
4.4 공감 toggle_episode_like(episode_id)
| 순서 | 처리 |
|---|---|
| 1 | 자기 사연이 아닌지, 경매가 OPEN인지 확인 |
| 2 | 요청자가 해당 경매의 주최자면 weight = 50, 아니면 10 |
| 3 | 행이 없으면 삽입, 있으면 삭제 |
점수를 직접 더하거나 빼지 않는다. 최종 점수는 §5의 뷰에서 집계하므로 연타·동시 조작으로 값이 어긋날 수 없다 (F4 완료 조건 6).
5. 뷰
5.1 v_episode_scores — 사연 최종 점수
episode_id
auction_id
user_id
bid_amount -- 본인이 건 포인트
like_weight_sum -- 받은 공감 가중치 합
total_score -- bid_amount + like_weight_sum
like_count
created_at -- 동점 판정용
낙찰자 선정과 랭킹 표시 모두 이 뷰를 쓴다.
ORDER BY total_score DESC, created_at ASC
F5 3.2.1의 동점 기준이 이 정렬 하나로 표현된다.
5.2 v_auction_summary — 목록·카드용
auction_id, title, thumbnail, end_at, status
episode_count -- 모인 사연 수 (인기순 기준)
top_score -- v_episode_scores 최고점
정렬 기준을 모두 이 뷰에서 처리한다 (F2).
| 정렬 | 기준 |
|---|---|
| 마감 임박순 (기본) | end_at ASC |
| 인기순 | episode_count DESC |
| 최신순 | created_at DESC |
찜 수는 이 뷰에 포함하지 않는다. 공개 지표가 아니다.
6. RLS 정책
관계 판정을 DB에서 강제한다 (P6). 00-관계-판정 4절의 "클라이언트 판정과 서버 판정이 다르면 서버를 최종으로 한다"가 여기서 보장된다.
| 테이블 | 조회 | 생성 | 수정·삭제 |
|---|---|---|---|
auctions | 전체 공개 | 로그인 + 본인 address_id 보유 | user_id = auth.uid() AND 사연 0건 |
episodes | 전체 공개 | 로그인 + 경매 주최자가 아님 + 경매 OPEN | 작성자 본인 (삭제는 §4.3) |
episode_likes | 전체 공개 | 로그인 + 자기 사연 아님 + 경매 OPEN | 본인 행만 삭제 |
auction_favorites | 본인 것만 | 로그인 + 경매 주최자가 아님 | 본인 행만 삭제 |
points | 본인 것만 | RPC만 | 불가 (append-only) |
chat_rooms | 주최자 또는 낙찰자만 | RPC만 | 불가 |
messages | 해당 방 참여자만 | 해당 방 참여자만 | sender_id = auth.uid() |
notifications | 본인 것만 | 서버만 | 본인 읽음 처리만 |
addresses | 본인 것만 | 본인 | 본인 |
핵심 정책 두 가지
-- 주최자는 자기 경매에 사연을 쓸 수 없다 (F3)
episodes INSERT:
auth.uid() <> (SELECT user_id FROM auctions WHERE id = auction_id)
-- 채팅은 주최자와 낙찰자만 (F6)
chat_rooms SELECT:
auth.uid() IN (
(SELECT user_id FROM auctions WHERE id = auction_id),
(SELECT e.user_id FROM auctions a JOIN episodes e ON e.id = a.winning_episode_id
WHERE a.id = auction_id)
)
7. 기존 스키마 대비 주요 변경
7.1 배열 → 조인 테이블 (가장 중요한 변경)
| 기존 | 신규 |
|---|---|
auctions.favorites text[] | auction_favorites 테이블 |
episodes.likes text[] | episode_likes 테이블 |
왜 바꾸는가
배열은 통째로 읽어서 통째로 쓴다. 두 사람이 동시에 공감을 누르면 나중 요청이 앞선 요청을 덮어쓴다.
A 읽기 [x] → A 쓰기 [x, A]
B 읽기 [x] → B 쓰기 [x, B] ← A의 공감이 사라진다
기존 서비스가 "클릭 시점에 최신 값을 다시 조회한다"는 우회 처리를 두었던 이유가 이것이다. 조인 테이블 + UNIQUE 제약으로 바꾸면 삽입·삭제만으로 끝나고 경합 자체가 사라진다.
F4·F7의 "연타·동시 조작에도 값이 어긋나지 않는다" 완료 조건이 구조로 해결된다.
7.2 그 외
| 항목 | 기존 | 신규 | 이유 |
|---|---|---|---|
users.role | text NOT NULL | 없음 | 사용자 유형 개념 삭제 (A1) |
| 경매 상태 | ongoing/urgent/OPEN/CLOSED 혼재 | OPEN/CLOSED | 마감 임박은 파생 상태 (P5) |
| 채팅 참여자 | seller_id, buyer_id | 없음 — 경매에서 도출 | 낙찰자 한정으로 단순화 (F6) |
| 공감 가중치 | 코드 상수 | episode_likes.weight 저장 | 해제 시 정확한 차감 보장 |
| 사연 점수 | episodes.bid_point에 직접 가산 | bid_amount + 뷰 집계 | 동시성·정합성 |
| 찜 수 | favorite_count 저장 + 인기순 기준 | 비공개 | 찜은 개인 북마크. 인기순은 사연 수 기준 |
| 잔액 | users.point + points.balance_after 이중 | users.point_balance 단일 출처 | 불일치 방지 |
| 푸시 구독 | users.subscription JSON | 없음 | 푸시 알림을 만들지 않는다 (F9) |
| 알림 | 테이블 없음 | notifications 신설 | 알림 목록 필요 (F9) |
| 사연 중복 | 애플리케이션 검사 | UNIQUE (auction_id, user_id) | DB 보장 |
8. 인덱스
| 테이블 | 인덱스 | 용도 |
|---|---|---|
auctions | (status, end_at) | 마감 임박 정렬, 마감 대상 조회 |
auctions | (user_id) | 내 경매 (F8) |
episodes | UNIQUE (auction_id, user_id) | 사연 1개 제한 + 관계 판정 |
episodes | (auction_id, created_at) | 목록·동점 정렬 |
episodes | (user_id) | 내 사연 (F8) |
episode_likes | UNIQUE (episode_id, user_id) | 중복 방지 |
auction_favorites | UNIQUE (auction_id, user_id) / (user_id) | 중복 방지 / 찜 목록 |
points | (user_id, created_at DESC) | 포인트 내역 |
messages | (chat_room_id, created_at) | 대화 조회 |
notifications | (user_id, created_at DESC) / (user_id) WHERE read_at IS NULL | 목록 / 미읽음 배지 |
episodes의UNIQUE (auction_id, user_id)는 제약이자 참여자 관계 판정용 인덱스다. 경매 상세에 들어갈 때마다 이 조회가 발생한다 (00-관계-판정3.2).
9. 마감 처리 실행
auctions.end_at이 지난 경매를 주기적으로 찾아 close_auction()을 호출한다.
| 항목 | 내용 |
|---|---|
| 실행 | Supabase 스케줄러 (pg_cron) |
| 주기 | 1분 |
| 대상 | status = 'OPEN' AND end_at <= now() |
| 중복 방지 | status 변경이 트랜잭션 안에서 일어나므로 두 번 처리되지 않는다 |
화면에서는 end_at이 지나면 처리 완료 전이라도 마감 상태로 표시하고 참여 액션을 차단한다 (F5 예외 처리).
10. 포인트 경제 요약 — 확정
| 흐름 | 금액 | 시점 |
|---|---|---|
| 발행 — 가입 보너스 | +5,000 P | 가입 시 1회 |
| 차감 — 입찰 | − 입찰액 | 사연에 포인트를 걸 때 |
| 반환 — 미낙찰·유찰·사연 삭제 | + 전액 | 마감 시 또는 삭제 시 |
| 이전 — 낙찰 | 낙찰자 → 주최자 | 마감 시 |
가입 보너스 지급 방식
SIGNUP_BONUS는 가입 시점 트리거로 지급한다. 스케줄러(크론)로 처리하지 않는다.
크론은 실행 주기만큼 지급이 늦어져, 가입 직후 첫 참여 시 포인트가 없는 상태가 발생할 수 있다. 1회성 지급은 사건이 일어난 시점에 처리한다.
11. 고정 상수
코드가 아니라 여기가 단일 출처다. 값이 바뀌면 이 표를 먼저 고친다.
11.1 입찰
| 항목 | 값 |
|---|---|
| 시작 | 1,000 P |
| 단위 | +500 P |
| 상한 | 3,000 P |
| 허용 값 | 1000 · 1500 · 2000 · 2500 · 3000 |
11.2 공감 가중치
| 공감한 사람 | 가중치 |
|---|---|
| 주최자 | 50 P |
| 그 외 | 10 P |
11.3 경매 기간
| 항목 | 값 |
|---|---|
| 선택지 | 1일 · 3일 · 7일 (버튼 선택) |
| 기본값 | 1일 — 회전이 빠른 쪽을 기본으로 둔다 |
| 마감 시각 | 등록 시점 + 선택한 기간 |
| 마감 임박 알림 | 기간에 비례한 3단계 (아래) |
마감 임박 알림 — 단계
단계는 3일 · 1일 · 1시간 셋으로 고정이다. 경매마다 분기하지 않고, 경매 전체 기간보다 짧은 단계만 보낸다는 규칙 하나로 결정된다.
| 등록 기간 | 3일 전 | 1일 전 | 1시간 전 | 합계 |
|---|---|---|---|---|
| 7일 | O | O | O | 3회 |
| 3일 | - | O | O | 2회 |
| 1일 | - | - | O | 1회 |
기간과 같거나 긴 단계를 제외하는 이유 — 3일 경매에 '3일 전' 단계를 허용하면 등록되는 순간 발송된다. 임박이라는 말이 의미를 잃는다.
단계를 지난 뒤 참여한 사람에게는 그 단계를 보내지 않는다.
episodes.created_at < auctions.end_at - 단계
마감 임박 알림은 잊고 있을 수 있는 사람을 깨우는 것이다 (F9 2). 방금 사연을 쓴 사람은 마감이 임박한 것을 이미 알고 있으므로 깨울 대상이 아니다. 마감 30분 전에 참여한 사람에게 "1시간 전이에요"를 보내면 문구도 사실과 다르다.
episodes.created_at은 수정해도 갱신하지 않으므로(F5 3.2.1) 기준으로 삼기에 안전하다.
한 번에 한 단계만 보낸다. 정상 운영에서는 단계마다 스케줄러가 먼저 처리하므로 겹치지 않지만, 스케줄러가 며칠 멈췄다 재개하면 한 사람이 세 단계를 동시에 만족한다. 그때 "3일 전이에요"를 30분 전에 보내지 않도록 가장 급한 단계 하나만 고른다.
11.4 글자 수
| 항목 | 범위 |
|---|---|
| 경매 제목 | 1 ~ 50자 |
| 경매 설명 | 1 ~ 500자 |
| 사연 제목 | 2 ~ 50자 |
| 사연 내용 | 5 ~ 1,000자 |
경매 쪽은 DB 가
btrim(...) > 0만 강제한다. 상한은 애플리케이션이 지킨다. 사연과 달리 하한을 2자·5자로 올리지 않은 것은, 제목이 물건 이름 한 글자여도 말이 되기 때문이다 (F1 4.2 는 "길이 초과"만 요구한다).
11.5 이미지
| 항목 | 값 |
|---|---|
| 저장소 | Supabase Storage 공개 버킷 |
| 장수 | 경매당 1 ~ 3장 |
| 용량 | 장당 최대 5 MB |
| 확장자 | jpg · png · webp |
경매 삭제 시 해당 이미지도 함께 정리한다.
11.6 마감 처리
| 항목 | 값 |
|---|---|
| 실행 | pg_cron |
| 주기 | 1분 |