ERD 설계 - 100-hours-a-week/KTB4-5th-wiki GitHub Wiki

목차

ERD 다이어그램

ERDCloud 링크

users

한 줄 역할

서비스 회원의 프로필·상태·조리 완료 누적 횟수와 v1 로컬 인증 정보를 저장한다.

컬럼

2026-09-07 ERD와 동기화한 설계 정의입니다. 운영 DB 적용 완료를 뜻하지 않으며, 미결 사항은 아래에 구분합니다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
user_id BIGINT N PK 회원 ID - 회원을 식별하는 내부 대리키
nickname VARCHAR(10) N — 닉네임 - 서비스에서 표시하는 2~10자 회원 이름. UNIQUE(nickname). 탈퇴 시 치환값도 중복 불가
profile_image_key VARCHAR(1024) N — 프로필 이미지 URL - 미등록 시 기본 이미지 URL 저장
status VARCHAR(20) N — 회원 상태 - ACTIVE: 활성, WITHDRAWN: 탈퇴. CHECK (status IN ('ACTIVE','WITHDRAWN'))
기본값/생성식: 'ACTIVE'
cooking_count INT N — 만든 요리 수 - 재고 차감을 완료한 누적 횟수. CHECK (cooking_count >= 0)
기본값/생성식: 0
created_at DATETIME(6) N — 회원 생성 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
updated_at DATETIME(6) N — 회원 정보 최종 수정 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
deleted_at DATETIME(6) Y — 회원 탈퇴 일시. 활성 회원은 NULL
login_id VARCHAR(10) Y — v1 영문 숫자 2~10자. UNIQUE(login_id). password_hash와 모두 NULL 또는 모두 NOT NULL CHECK. OAuth-only는 NULL
password_hash VARCHAR(60) Y — bcrypt 해시. login_id와 동시 존재 CHECK. 탈퇴 시 둘 다 NULL. 일반 응답에서 제외
password_changed_at DATETIME(6) Y — 기존 local_accounts의 변경 시각 보존. 이 시각보다 먼저 발급된 토큰 무효화 판단. OAuth-only는 NULL

제약조건

  • PK: user_id
  • FK: 없음.
  • UNIQUE: UNIQUE (nickname) — 활성/탈퇴 치환 닉네임 중복 차단
  • CHECK: CHECK (status IN ('ACTIVE','WITHDRAWN')) — 정의되지 않은 회원 상태 차단; CHECK (cooking_count >= 0) — 누적 횟수 음수 차단

관계

  • social_accounts 1:0..N — 제공자별 인증 연결.
  • v1 로컬 인증은 users.login_id/password_hash에 통합. local_accounts는 이전 구조로만 보존.
  • notification_preferences, refrigerator_members, notification_recipients, cooking_records 각각 1:0..N.

생명주기

  • 생성: 가입 시 인증 계정·개인 냉장고·OWNER 참여와 함께 생성.
  • 변경: 닉네임·프로필·상태 변경, 조리 성공 시 cooking_count 증가.
  • 삭제: WITHDRAWN 전환·deleted_at 기록·개인정보 익명화, 인증 연결 제거.
  • 모든 기기의 RT를 폐기하고 푸시 구독을 비활성화한다. 만료 전 AT도 탈퇴 계정의 접근을 차단한다.
  • OWNER라면 남은 유효 구성원 중 참여 시각이 가장 빠른 구성원에게 자동 위임한다(동률은 refrigerator_member_id ASC). 마지막 구성원이면 refrigerators.deleted_at을 기록한다.
  • 공유 재고·조리 기록 등을 회원탈퇴와 함께 연쇄 물리 삭제하지 않는다. 참조 및 보관 목적·기간을 확인해 후속 정리한다.
  • 보관: 공유 데이터 참조에 필요한 최소 식별자를 유지하고 목적·기간 종료 후 파기 검토.

설계 결정

  • 회원과 인증 수단을 구분해 OAuth 제공자가 달라도 동일 회원 본체를 참조한다.
  • cooking_count는 매번 기록을 집계하지 않기 위한 반정규화 값이다. 조리 기록·재고 차감과 같은 트랜잭션으로 증가시켜 불일치를 막는다.
  • 로컬 인증 통합을 설계에 반영했다. login_id UNIQUE 및 login_id/password_hash 동시 NULL·NOT NULL CHECK를 적용한다. JOIN 성능 우위는 측정 없이 단정하지 않는다.
  • 기존 password_changed_at은 users에 보존한다. 비밀번호 변경 이전 발급 토큰의 무효화 기준이며 OAuth-only는 NULL이다.
  • 탈퇴 시 login_id/password_hash/password_changed_at을 모두 NULL로 비운다.
  • 확장: 인증 제공자 추가는 social_accounts에서 처리한다. 프로필·인증을 API 응답에서 명시적으로 선택해 민감 필드 노출을 막는다.
  • 내부 PK·참조 FK의 BIGINT는 식별자 타입을 일치시키는 기존 선택이다. DATETIME(6)은 소수초 정밀도를 보존하지만 시간만으로 사건 순서를 보장하지 않는다.
  • nickname (VARCHAR(10)): 서비스 정책이 2~10자이므로 상한과 동일한 VARCHAR(10). UNIQUE로 중복 닉네임을 차단한다.
  • profile_image_key의 현행 ERD 설명은 URL 저장이다. 이전 카드의 S3 객체 키 설명과 차이가 있어 API·저장소 계약에서 최종 형식을 확인한다. 타입·NULL은 현행 ERD에 맞췄다.
  • status (VARCHAR(20)): 현재 ACTIVE/WITHDRAWN 값과 향후 상태 확장을 수용하는 VARCHAR(20). 허용값은 CHECK로 제한한다.
  • cooking_count (INT): 누적 횟수 범위를 안정적으로 수용한다. 음수는 CHECK로 차단한다.
  • 인덱스: 주요 조회는 닉네임 중복 확인과 회원 PK 조회이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • 실제 v1 데이터 이전·배포 분리 여부 검증. login_id/password_hash 동시 NULL 또는 동시 NOT NULL은 확정 설계.
  • local_accounts의 created_at/updated_at을 users의 회원 시각으로 덮어쓰지 않는다. 이전 전 원본 백업과 인증 시각 보존 범위를 확정한다. 실제 테이블 삭제는 미실행. v2·v3 로컬 로그인은 비활성.
  • 익명화 닉네임의 10자·유일성 충족 방식과 보관 기간.

social_accounts

한 줄 역할

카카오·구글의 계정 식별자를 서비스 회원과 연결한다.

컬럼

2026-09-07 ERD와 동기화한 설계 정의입니다. 운영 DB 적용 완료를 뜻하지 않으며, 미결 사항은 아래에 구분합니다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
social_account_id BIGINT N PK 소셜 계정 연결 ID
user_id BIGINT N FK 연결된 서비스 회원 ID. UNIQUE(user_id, provider)
provider VARCHAR(20) N — OAuth 제공자 - KAKAO, GOOGLE. CHECK (provider IN ('KAKAO','GOOGLE'))
provider_user_id VARCHAR(255) N — OAuth 제공자 사용자 식별자. UNIQUE(provider, provider_user_id)
created_at DATETIME(6) N — OAuth 계정 연결 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
updated_at DATETIME(6) N — OAuth 계정 정보 최종 수정 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
provider_email VARCHAR(255) Y — OAuth 제공자가 전달한 이메일 스냅샷. 로그인 식별자는 아니며 UNIQUE 미적용. 제공자가 이메일을 반환한 경우 저장하고, 성공 로그인 시 최신 값으로 갱신

제약조건

  • PK: social_account_id
  • FK: user_id → users.user_id
  • UNIQUE: UNIQUE (provider, provider_user_id) — 동일 소셜 계정의 중복 가입 차단; UNIQUE (user_id, provider) — 한 회원에 같은 제공자 계정 중복 연결 차단
  • CHECK: CHECK (provider IN ('KAKAO','GOOGLE')) — 지원하지 않는 공급자 값 차단

관계

  • users N:1 — 각 인증 연결은 한 회원 소속; 회원은 제공자별 0..N개 연결.

생명주기

  • 생성: OAuth 신규 가입 또는 향후 승인된 계정 연동 시 생성.
  • 변경: 성공 로그인 시 제공자 이메일 스냅샷 등 갱신.
  • 삭제: 탈퇴 시 우리 서비스의 OAuth 연결을 제거한다. 외부 제공자 계정은 삭제하지 않는다.
  • 보관: 현재 연결만 유지한다. 같은 OAuth 재가입은 새 users와 새 연결을 생성하며 과거 데이터는 자동 복구하지 않는다.

설계 결정

  • (provider, provider_user_id)는 제공자 내부의 식별자 범위를 함께 표현해 중복 연결을 막는다.
  • (user_id, provider)는 회원당 같은 제공자 연결을 최대 하나로 제한한다. 대리 PK와 두 업무 유일성은 역할이 다르다.
  • provider_email은 변경·누락·제공자 간 중복이 가능하므로 로그인 키나 UNIQUE로 사용하지 않는다.
  • 확장: 새 제공자는 허용값과 인증 연동을 추가한다. 다중 계정 연동 UX는 후속 요구다.
  • 내부 PK·참조 FK의 BIGINT는 식별자 타입을 일치시키는 기존 선택이다. DATETIME(6)은 소수초 정밀도를 보존하지만 시간만으로 사건 순서를 보장하지 않는다.
  • provider (VARCHAR(20)): KAKAO·GOOGLE 및 향후 짧은 제공자 코드를 수용하는 VARCHAR(20).
  • provider_user_id (VARCHAR(255)): 제공자마다 숫자·문자 형식과 길이가 달라 범용 VARCHAR(255)를 사용한다.
  • provider_email (VARCHAR(255)): 제공자마다 이메일을 반환하지 않을 수 있어 Null을 허용한다. 여러 제공자가 같은 이메일을 반환하거나 이메일이 변경될 수 있어 UNIQUE를 적용하지 않는다.
  • 인덱스: 주요 조회는 provider와 provider_user_id 조합 로그인 조회이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • 여러 OAuth 연결·해제의 본인 확인과 계정 충돌 처리.
  • 제공자별 식별자 최대 길이와 이메일 갱신 계약 확인.

refrigerators

한 줄 역할

개인·공유 냉장고의 재고 공간과 등록 상한을 저장한다.

컬럼

2026-09-08 냉장고 soft delete 정책과 deleted_at을 반영한 설계 정의입니다. 운영 DB 적용 완료를 뜻하지 않으며, 미결 사항은 아래에 구분합니다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
refrigerator_id BIGINT N PK 냉장고 ID - 냉장고를 식별하는 대리키
name VARCHAR(20) N — 냉장고 이름 - 생성 시 소유자 닉네임으로 초기화, 이후 독립 관리
capacity SMALLINT N — 재고 등록 상한 - 냉장고당 저장 가능한 재고 행의 최대 개수. 기본 100종. CHECK (capacity > 0)
기본값/생성식: 100
created_at DATETIME(6) N — 냉장고 생성 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
updated_at DATETIME(6) N — 냉장고 정보 최종 수정 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
deleted_at DATETIME(6) Y — 마지막 구성원 탈퇴 시 기록하는 삭제 시각. NULL은 사용 중. 기본값 NULL
expired_count INT N — 이번 달 만료 재고 행 누적 수. 기본값 0. CHECK (expired_count >= 0). 재고 삭제 후에도 유지
expired_count_month CHAR(7) N — 집계 연월 YYYY-MM. 예: 2026-09. 생성 시 서버가 현재 월 입력. 과거 월 기록은 보관하지 않음

제약조건

  • PK: refrigerator_id
  • FK: 없음.
  • UNIQUE: 추가 업무 UNIQUE 없음.
  • CHECK: CHECK (capacity > 0) — 등록 상한이 0 또는 음수가 되는 값 차단

관계

  • refrigerator_members, ingredients, invite_codes, notifications, cooking_records, push_notifications 각각 1:0..N.
  • users와는 refrigerator_members를 통해 N:M; 소유자는 role=OWNER로 표현.

생명주기

  • 생성: 회원 가입 시 기본 개인 냉장고 생성.
  • 변경: 이름은 생성 시 닉네임으로 초기화하고 이후 독립 변경; capacity로 등록 상한 검증.
  • 삭제: OWNER 회원탈퇴 시 남은 유효 구성원 중 created_at ASC, refrigerator_member_id ASC의 첫 구성원에게 자동 위임하고 냉장고·재고는 유지한다. 수동 소유권 이전 기능과는 별개다.
  • 마지막 구성원이 탈퇴하면 deleted_at을 즉시 기록해 soft delete한다. 복구 유예 기능은 제공하지 않는다.
  • deleted_at이 있는 냉장고는 일반 조회·가입·재고 변경·추천·푸시 대상에서 제외한다.
  • hard delete: 관련 기록의 보관 목적·기간이 끝나고 FK 참조를 자식부터 정리한 뒤 냉장고를 마지막에 삭제한다. 탈퇴 요청에서 전체 데이터를 CASCADE 삭제하지 않는다.
  • 보관: 공유 가입 중 개인 냉장고는 비활성 상태로 보존하고 공유 탈퇴 시 다시 활성화.

월별 만료 집계

  • expired_count_month는 카운트가 어느 연·월에 속하는지 구분한다. 유효한 YYYY-MM 형식과 01~12월을 검증한다.
  • 월이 바뀌면 기존 값을 0으로 초기화하고 새 월을 설정한 뒤 그 달 만료분을 누적한다. 두 값은 냉장고 단위 잠금 아래 원자적으로 갱신한다.
  • 매월 1일 초기화 작업이 누락돼도 집계 월 검증으로 지난달 숫자를 그대로 반환하지 않는다.
  • 삭제된 재고의 누적 수는 줄이지 않는다. 미래 만료 예정 수·현재 남아 있는 만료 재고 수와 다르다.
  • 과거 월별 이력 테이블은 추가하지 않는다.

구현 검증 필요

  • 두 집계 필드만으로 재고별 중복·누락 집계가 방지되지는 않는다. 만료 반영 여부 판별 및 재시도·삭제·수정과의 원자성 처리는 구현 전에 설계·검증한다.

설계 결정

  • OWNER 정보를 참여 관계에만 두어 refrigerators 소유자 FK와의 중복·불일치를 막는다.
  • capacity는 개별 재료 수량이 아닌 재고 행 등록 상한이다. 여러 행의 개수 제한은 등록 트랜잭션에서 검증한다.
  • 확장: 상한 변경은 capacity 정책으로 대응한다. 복구 요구가 생기면 삭제 대기 상태·복구 기한을 함께 설계한다.
  • 내부 PK·참조 FK의 BIGINT는 식별자 타입을 일치시키는 기존 선택이다. DATETIME(6)은 소수초 정밀도를 보존하지만 시간만으로 사건 순서를 보장하지 않는다.
  • name (VARCHAR(20)): 현재 닉네임 기반 이름과 직접 편집 이름의 최대 20자 정책에 맞춘 VARCHAR(20).
  • capacity (SMALLINT): 기본 100종이며 SMALLINT 범위면 충분하다. 음수·0은 CHECK로 차단한다.
  • 인덱스: 주요 조회는 PK별 냉장고 조회 및 참여 관계를 통한 접근 검증이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • refrigerator_type VARCHAR(20), PERSONAL/SHARED 구분은 PL 재확인 대상. 아직 ERD 컬럼에 추가하지 않았다.
  • 팀의 기존 개인 냉장고 공유 방안과 API 초안의 별도 생성 흐름을 확인한 뒤 생성·전환 흐름을 확정한다. OWNER 회원탈퇴 처리는 위 생명주기 정책으로 확정했다. 타입 제안만으로 별도 생성을 확정하지 않는다.
  • 보관 기간의 구체적인 숫자와 정리 작업 주기는 운영 정책으로 별도 결정한다. 미확정 상태에서는 hard delete를 실행하지 않는다.
  • 자동 위임·마지막 구성원 판단은 냉장고와 참여 관계를 잠근 동일 트랜잭션에서 재검증하고, 유지되는 냉장고의 유효 OWNER가 한 명인지 검증한다.

refrigerator_members

한 줄 역할

회원과 냉장고의 참여 관계·역할·현재 선택된 냉장고를 저장한다.

컬럼

2026-09-07 ERD와 동기화한 설계 정의입니다. 운영 DB 적용 완료를 뜻하지 않으며, 미결 사항은 아래에 구분합니다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
refrigerator_member_id BIGINT N PK 참여자 ID - 냉장고 참여를 식별하는 대리키
refrigerator_id BIGINT N FK 냉장고 ID - 참여 대상 냉장고
user_id BIGINT N FK 참여 회원 ID. UNIQUE(refrigerator_id, user_id)
is_active TINYINT(1) N — 현재 사용하는 냉장고 여부. 전환 시 개인 관계 보존. CHECK (is_active IN (0,1))
기본값/생성식: 0
active_marker BIGINT Y — is_active=1이면 user_id, 아니면 NULL. UNIQUE(active_marker)는 회원당 활성 냉장고 최대 1개 제한. 유형·가입 자격을 나타내지 않음
기본값/생성식: GENERATED ALWAYS AS (IF(is_active=1,user_id,NULL)) STORED
role VARCHAR(20) N — 역할 - OWNER: 소유자, MEMBER: 참여자. 소유권은 이 값으로 표현(냉장고에 소유자 FK 중복 저장 안 함). CHECK (role IN ('OWNER','MEMBER'))
기본값/생성식: MEMBER
created_at DATETIME(6) N — 참여 시각
기본값/생성식: CURRENT_TIMESTAMP(6)
updated_at DATETIME(6) N — 최종 수정 일시
기본값/생성식: CURRENT_TIMESTAMP(6)

제약조건

  • PK: refrigerator_member_id
  • FK: refrigerator_id → refrigerators.refrigerator_id; user_id → users.user_id
  • UNIQUE: UNIQUE (refrigerator_id, user_id) — 동일 냉장고 중복 참여 차단; UNIQUE (active_marker) — 회원별 활성 냉장고 최대 한 개 제한
  • CHECK: CHECK (is_active IN (0,1)) — 불리언 범위 강제; CHECK (role IN ('OWNER','MEMBER')) — 정의되지 않은 역할 차단

관계

  • users N:1; refrigerators N:1 — 두 엔티티 사이 N:M 관계의 연결 테이블.

생명주기

  • 생성: 개인 냉장고 생성 또는 공유 가입 시 참여 관계 생성.
  • 변경: 역할 이전과 현재 선택 냉장고 전환을 트랜잭션으로 처리.
  • OWNER 회원탈퇴: 남은 유효 구성원을 created_at ASC, refrigerator_member_id ASC로 정렬해 첫 구성원에게 자동 위임한다. 기존 OWNER 해제와 후임 지정·회원탈퇴를 같은 트랜잭션으로 처리한다. 수동 위임 API는 제공하지 않는다.
  • 후보에서 탈퇴 회원을 제외한다. is_active는 현재 선택 여부이므로 위임 자격을 판정하는 필터로 쓰지 않는다.
  • 남은 구성원이 없으면 refrigerators.deleted_at을 기록하고 해당 냉장고의 접근·가입·추천·푸시를 차단한다.
  • 삭제: 기준 정책은 탈퇴 참여 관계 비활성화·필요 이력 보존. 기존 정의의 즉시 행 삭제와 충돌하므로 구현 결정 필요.
  • 보관: 공유 가입 시 개인 관계를 보존한다. 종료한 멤버십의 보존 방식은 미확정.

설계 결정

  • 역할은 사용자 자체가 아니라 냉장고 참여마다 달라 관계 테이블에 둔다.
  • 대리 PK는 참여 관계를 한 값으로 식별하고 (refrigerator_id, user_id) UNIQUE는 중복 참여를 막는다.
  • active_marker는 활성 시 user_id, 아니면 NULL인 생성 컬럼이다. UNIQUE는 활성 냉장고를 최대 하나로 제한한다. 정확히 하나의 존재는 애플리케이션 전환 트랜잭션이 보장해야 한다.
  • is_active는 현재 보는 냉장고 표시이며 탈퇴·추방 상태와 동일하지 않다. 정원 4명·OWNER 권한은 여러 행을 확인하는 트랜잭션 규칙이다.
  • 확장: 종료 이력 필요 시 상태·종료 시각 또는 별도 이벤트를 검토하되 재가입과 UNIQUE 충돌을 함께 해결한다.
  • 내부 PK·참조 FK의 BIGINT는 식별자 타입을 일치시키는 기존 선택이다. DATETIME(6)은 소수초 정밀도를 보존하지만 시간만으로 사건 순서를 보장하지 않는다.
  • is_active (TINYINT(1)): MySQL에서 불리언을 표현한다. 0/1 이외 값은 CHECK가 필요하다.
  • role (VARCHAR(20)): OWNER·MEMBER 코드와 향후 역할을 수용하는 VARCHAR(20). CHECK로 허용값을 제한한다.
  • 인덱스: 주요 조회는 냉장고별 구성원 목록과 회원별 현재 활성 냉장고이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • 참여 종료 상태를 표현할 컬럼 또는 이력 테이블. is_active만으로 대체하지 않음.
  • 회원탈퇴와 자동 위임은 냉장고·관련 참여 관계를 잠그고 남은 구성원과 유효 OWNER 한 명을 재검증한다. 가입·추방·전환도 같은 냉장고 단위로 직렬화해야 한다. 상세 SQL과 잠금 순서는 구현에서 검증한다.

invite_codes

한 줄 역할

공유 냉장고에 참여할 때 사용하는 6자리 초대 코드를 저장한다.

컬럼

2026-09-07 ERD와 동기화한 설계 정의입니다. 운영 DB 적용 완료를 뜻하지 않으며, 미결 사항은 아래에 구분합니다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
invite_code_id BIGINT N PK 초대 코드 ID - 초대 코드를 식별하는 대리키
code CHAR(6) N — 초대 코드 - 영문 대문자와 숫자 6자리. UNIQUE(code). CHECK (code REGEXP '^[A-Z0-9]{6}$')
created_at DATETIME(6) N — 생성 일시 - 코드 발급 시각
기본값/생성식: CURRENT_TIMESTAMP(6)
refrigerator_id BIGINT N FK 냉장고 ID - 초대 코드가 속한 냉장고. 재발급 가능하므로 UNIQUE 미적용

제약조건

  • PK: invite_code_id
  • FK: refrigerator_id → refrigerators.refrigerator_id
  • UNIQUE: UNIQUE (code) — 동시에 발급된 초대 코드 충돌 차단
  • CHECK: CHECK (code REGEXP '^[A-Z0-9]{6}$') — 정확히 대문자·숫자 6자리만 허용

관계

  • refrigerators N:1 — 같은 냉장고에서 재발급한 코드가 존재할 수 있음.
  • 발급 권한은 refrigerator_members의 OWNER 확인; 현재 user_id FK는 없음.

생명주기

  • 생성: OWNER 권한 확인 후 충돌하지 않는 코드 발급.
  • 변경: 사용·재발급·만료 시 유효성 변경 정책 적용; 현재 표에 상태 컬럼 없음.
  • 삭제: 냉장고 접근 종료 시 무효화. 물리 삭제 시점은 보관 정책에 맞춰 결정.
  • 보관: 유효 시간·사용 횟수·추적 보관 기간 미확정; 현 컬럼만으로 폐기 이력을 표현할 수 없음.

설계 결정

  • code UNIQUE로 코드 조회가 한 대상을 가리키게 한다. refrigerator_id UNIQUE는 재발급을 제한하므로 두지 않는다.
  • 대리 PK는 발급 사건을 식별하고 code는 사용자가 입력하는 업무 키다.
  • 확장: 만료·폐기 이력 요구가 확정되면 expires_at/revoked_at 등의 필요성을 검토한다.
  • 내부 PK·참조 FK의 BIGINT는 식별자 타입을 일치시키는 기존 선택이다. DATETIME(6)은 소수초 정밀도를 보존하지만 시간만으로 사건 순서를 보장하지 않는다.
  • code (CHAR(6)): 대문자·숫자 정확히 6자리 고정값이므로 CHAR(6). UNIQUE로 충돌을 차단한다.
  • 인덱스: 주요 조회는 code 일치 조회와 냉장고별 발급 관리이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • 유효 시간, 일회용·다회용, 재발급 시 이전 코드 처리, 보관 기간.
  • 문자 대소문자 검사와 collation 일치; 정원 4명 검증과 가입의 원자성.

ingredients

한 줄 역할

냉장고에 있는 재료의 현재 수량·측정량·보관 방식·유통기한을 저장한다.

컬럼

2026-09-07 ERD와 동기화한 설계 정의입니다. 운영 DB 적용 완료를 뜻하지 않으며, 미결 사항은 아래에 구분합니다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
ingredient_id BIGINT N PK 식재료 ID - 식재료를 식별하는 대리키
refrigerator_id BIGINT N FK 냉장고 ID - 냉장고를 식별하는 대리키
name VARCHAR(100) N — 재료 이름 - 사용자가 입력하거나 OCR이 인식한 원문
category VARCHAR(20) N — 카테고리 - VEGETABLE, FRUIT, MEAT, SEAFOOD, DAIRY, TOFU_BEAN, GRAIN_NOODLE, PROCESSED, SEASONING, BEVERAGE, ETC. CHECK로 허용값 강제
storage_type VARCHAR(20) N — 보관 방법 - REFRIGERATED: 냉장, FROZEN: 냉동. CHECK (storage_type IN ('REFRIGERATED','FROZEN'))
기본값/생성식: 'REFRIGERATED'
quantity SMALLINT Y — 개수 측정값. 예) 한개, 두개. measure_type=COUNT일 때 1~100, measure_type=WEIGHT일 때 NULL
weight_value DECIMAL(10, 3) Y — measure_type=WEIGHT일 때만 필수인 총 측정량. 예) 우유 500ml에서 500. COUNT이면 NULL. G/ML 기준으로 저장
weight_unit VARCHAR(10) N — measure_type=COUNT이면 NONE, WEIGHT이면 G 또는 ML. kg·L은 저장 전 G·ML로 환산
기본값/생성식: 'NONE'
measure_type VARCHAR(10) N — COUNT: 개수 기준(quantity 사용, weight_value NULL, weight_unit NONE) / WEIGHT: 용량 기준(weight_value 필수, weight_unit G 또는 ML)
기본값/생성식: 'COUNT'
expiration_date DATE N — 유통기한 - 날짜만 저장, 해당일 23:59:59까지 유효
ingredient_image_key VARCHAR(512) Y — 실물 사진 키 - S3 객체 키. 미등록 시 NULL
created_at DATETIME(6) N — 생성 일시 - 등록 시각
기본값/생성식: CURRENT_TIMESTAMP(6)
updated_at DATETIME(6) N — 수정 일시
기본값/생성식: CURRENT_TIMESTAMP(6)

제약조건

  • PK: ingredient_id
  • FK: refrigerator_id → refrigerators.refrigerator_id
  • UNIQUE: 추가 업무 UNIQUE 없음.
  • CHECK: CHECK (measure_type IN ('COUNT','WEIGHT')) — 정의되지 않은 측정 방식 차단; CHECK (category IN ('VEGETABLE','FRUIT','MEAT','SEAFOOD','DAIRY','TOFU_BEAN','GRAIN_NOODLE','PROCESSED','SEASONING','BEVERAGE','ETC')) — 분류 코드 오염 차단; CHECK (storage_type IN ('REFRIGERATED','FROZEN')) — 지원 보관 방식만 허용; COUNT/WEIGHT 배타 조합 — 기존 제약의 NULL 누락을 보완하는 문서상 수정안: CHECK ((measure_type='COUNT' AND quantity IS NOT NULL AND quantity BETWEEN 1 AND 100 AND weight_value IS NULL AND weight_unit='NONE') OR (measure_type='WEIGHT' AND quantity IS NULL AND weight_value IS NOT NULL AND weight_value>0 AND weight_unit IN ('G','ML')))

관계

  • refrigerators N:1 — 재료는 한 냉장고에 소속.
  • 알림 대상 연결 방식은 미정이며 현재 ingredients를 참조하는 알림 연결 테이블은 없다.
  • recipe_ingredients와 직접 FK 관계 없음 — 재고와 레시피 필요 재료는 다른 개념.

생명주기

  • 생성: 사용자가 직접 입력하거나 OCR 결과를 확인·확정한 뒤 생성.
  • 변경: 재료 편집·수량 변경·조리 차감. 재고 지문을 바꾸되 변경 즉시 AI 호출하지 않음.
  • 삭제: 소진 또는 사용자 삭제, 냉장고 최종 삭제 시 물리 제거.
  • 보관: 현재 재고만 보관; OCR 중간 결과와 변경 이력은 영속 저장하지 않는다.

설계 결정

  • 같은 이름이라도 유통기한·보관 방식이 다르면 별도 재고이므로 name UNIQUE를 두지 않는다.
  • measure_type과 배타적 측정 조합으로 개수·무게·부피가 섞이지 않게 한다. kg·L은 G·ML로 정규화한다.
  • 상태·D-day는 날짜에서 계산해 매일 중복 상태를 갱신하지 않는다.
  • capacity 상한·권한·동시 차감은 애플리케이션 트랜잭션으로 검증한다.
  • 확장: 실행 취소·감사가 필요해지면 재고 변경 이벤트, 포장 단위가 필요해지면 검증된 단위 변환 모델을 검토한다.
  • 내부 PK·참조 FK의 BIGINT는 식별자 타입을 일치시키는 기존 선택이다. DATETIME(6)은 소수초 정밀도를 보존하지만 시간만으로 사건 순서를 보장하지 않는다.
  • name (VARCHAR(100)): 사용자 입력·OCR 원문을 수용하면서 검색·인덱스 부담을 제한하는 VARCHAR(100).
  • category (VARCHAR(20)): 가장 긴 GRAIN_NOODLE 등을 수용하는 VARCHAR(20). CHECK로 목록 외 값을 차단한다.
  • storage_type (VARCHAR(20)): REFRIGERATED·FROZEN 코드에 충분한 VARCHAR(20). CHECK로 허용값을 제한한다.
  • quantity (SMALLINT): 정책 범위가 1~100이라 SMALLINT면 충분하다. WEIGHT 방식에서는 사용하지 않으므로 배타 조합 CHECK를 성립시키려면 NULL을 허용해야 한다.
  • weight_value (DECIMAL(10, 3)): G/ML 환산 후 소수 셋째 자리까지 정확히 합산하기 위한 DECIMAL(10,3). FLOAT/DOUBLE의 근사 오차를 피한다.
  • weight_unit (VARCHAR(10)): G·ML·NONE 코드만 저장하므로 VARCHAR(10). COUNT면 NONE, WEIGHT면 G/ML이다.
  • expiration_date (DATE): 만료 판단이 시각이 아니라 날짜 기준이므로 DATE.
  • ingredient_image_key (VARCHAR(512)): URL이 아닌 S3 객체 키를 저장하며 현재 키 규칙에 충분한 VARCHAR(512).
  • measure_type (VARCHAR(10)): COUNT·WEIGHT를 직접 표시해 quantity와 weight_value 중 무엇을 쓰는지 테이블만 보고 알 수 있게 한다.
  • 인덱스: 주요 조회는 냉장고별 목록에서 보관·카테고리 필터와 유통기한·이름 정렬이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • 기존 COUNT CHECK의 quantity NULL 허용 구멍과 WEIGHT CHECK의 quantity IS NULL 누락을 보완한 DDL 검증.
  • 유통기한 판단의 기준 시간대와 이미지 키 길이 정책 확인.

cooking_records

한 줄 역할

조리 성공 사건을 기록하고 같은 요청의 재고 중복 차감을 막는다.

컬럼

2026-09-07 ERD와 동기화한 설계 정의입니다. 운영 DB 적용 완료를 뜻하지 않으며, 미결 사항은 아래에 구분합니다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
cooking_record_id BIGINT N PK 재고 차감에 성공한 조리 완료 기록의 대리키
recommendation_id VARCHAR(64) N — 캐시된 추천 결과 식별자. 레시피 FK가 아님
recipe_name_snapshot VARCHAR(150) N — 적용 당시 사용자에게 표시된 요리명
idempotency_key VARCHAR(64) N — 동일 적용 요청의 중복 차감 방지. 반드시 UNIQUE 제약
cooked_at DATETIME(6) N — 재고 차감과 조리 기록이 완료된 시각
기본값/생성식: CURRENT_TIMESTAMP(6)
user_id BIGINT N FK 회원 ID - 회원을 식별하는 내부 대리키
refrigerator_id BIGINT N FK 냉장고 ID - 냉장고를 식별하는 대리키

제약조건

  • PK: cooking_record_id
  • FK: user_id → users.user_id; refrigerator_id → refrigerators.refrigerator_id
  • UNIQUE: UNIQUE (idempotency_key) — 재시도·중복 클릭에 의한 재고 이중 차감 차단
  • CHECK: 추가 CHECK 정의 없음.

관계

  • users N:1; refrigerators N:1 — 조리 행위자와 재고 소속.
  • [검토안] recipes N:0..1 — 전환기·기존 기록은 recipe_id NULL 허용.
  • recommendation_id는 캐시 식별자이며 물리 FK 없음.

생명주기

  • 생성: 현재 권한·재고 검증 후 기록 생성·재료 차감·cooking_count 증가를 같은 트랜잭션으로 처리.
  • 변경: 같은 멱등키 재시도는 같은 성공 결과 반환. 다른 사용자·냉장고·내용으로 재사용하면 충돌 거절.
  • 삭제: 멱등 재시도·운영 추적 보관 기간 종료 후 파기.
  • 보관: 당시 recipe_name_snapshot을 유지. 회원은 익명화 식별자로 참조 보존.

설계 결정

  • idempotency_key UNIQUE로 연속 클릭·재전송이 중복 조리가 되지 않게 한다. recommendation_id는 여러 요리에 공유되므로 UNIQUE가 아니다.
  • 요리명 스냅샷은 레시피 수정·캐시 퇴출 후에도 당시 기록을 설명하기 위한 의도적 중복이다.
  • cooking_records 명칭을 ERD에 반영했다. PK는 cooking_record_id, 완료 시각은 cooked_at이다. UI 버튼명 변경과 별도 결정이다.
  • 확장: recipe_id와 recipe_revision_snapshot 도입을 검토한다. 버전만으로 전체 사용 재료 차감 이력이 복원되지는 않는다.
  • 내부 PK·참조 FK의 BIGINT는 식별자 타입을 일치시키는 기존 선택이다. DATETIME(6)은 소수초 정밀도를 보존하지만 시간만으로 사건 순서를 보장하지 않는다.
  • recommendation_id (VARCHAR(64)): UUID/해시 기반 추천 식별자에 맞춘 VARCHAR(64). 레시피 FK가 아니다.
  • recipe_name_snapshot (VARCHAR(150)): 표시 당시 요리명을 보존하되 UI 입력 상한을 고려한 VARCHAR(150).
  • idempotency_key (VARCHAR(64)): UUID/해시 멱등 키에 맞춘 VARCHAR(64). UNIQUE가 중복 차감을 막는다.
  • 인덱스: 주요 조회는 idempotency_key 중복 요청 확인 및 회원 조리 이력 조회이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • 실제 구현 코드의 API·ORM·집계 참조와 마이그레이션은 별도 반영·검증한다.
  • recipe_id 및 recipe_revision_snapshot 타입·연결 계약, 과거 기록 NULL 처리.
  • 멱등 재시도 기한과 조리 기록 보관 기간.

notification_preferences

한 줄 역할

회원별 임박·만료 알림과 추천 알림의 수신 여부를 저장한다.

컬럼

2026-09-07 ERD와 동기화한 설계 정의입니다. 운영 DB 적용 완료를 뜻하지 않으며, 미결 사항은 아래에 구분합니다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
preference_id BIGINT N PK 알림 설정 ID - 대리키
user_id BIGINT N FK 회원 ID - 회원을 식별하는 내부 대리키
type VARCHAR(20) N — 알림 종류 - EXPIRATION, RECIPE. UNIQUE(user_id, type). CHECK (type IN ('EXPIRATION','RECIPE'))
is_enabled TINYINT(1) N — 수신 여부 - 켬/끔은 UPDATE, 행 삭제 안 함. CHECK (is_enabled IN (0,1))
기본값/생성식: TRUE
created_at DATETIME(6) N — 생성 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
updated_at DATETIME(6) N — 수정 일시
기본값/생성식: CURRENT_TIMESTAMP(6)

제약조건

  • PK: preference_id
  • FK: user_id → users.user_id
  • UNIQUE: UNIQUE (user_id, type) — 회원·알림 종류별 설정 한 행 보장
  • CHECK: CHECK (type IN ('EXPIRATION','RECIPE')) — 지원 알림 유형만 허용; CHECK (is_enabled IN (0,1)) — 불리언 범위 강제

관계

  • users N:1 — 회원별로 알림 종류마다 설정 한 행.

생명주기

  • 생성: 가입 시 지원 알림 종류별 기본 설정 생성.
  • 변경: 켜기·끄기는 is_enabled UPDATE.
  • 삭제: 회원 탈퇴 시 현재 설정 행 제거.
  • 보관: 설정 변경 이력은 현재 범위에 포함하지 않는다.

설계 결정

  • (user_id, type) UNIQUE로 한 종류의 상충된 설정을 막는다.
  • 수신 여부는 회원·알림 종류 관계의 속성이므로 users에 종류별 불리언 열을 반복 추가하지 않는다.
  • 확장: 새 알림 종류가 실제 도입되면 허용 코드와 초기 설정 생성 규칙을 확장한다.
  • 내부 PK·참조 FK의 BIGINT는 식별자 타입을 일치시키는 기존 선택이다. DATETIME(6)은 소수초 정밀도를 보존하지만 시간만으로 사건 순서를 보장하지 않는다.
  • type (VARCHAR(20)): EXPIRATION·RECIPE와 향후 알림 코드를 수용하는 VARCHAR(20).
  • is_enabled (TINYINT(1)): MySQL에서 불리언을 표현한다. 0/1 이외 값은 CHECK가 필요하다.
  • 인덱스: 주요 조회는 user_id와 type별 수신 허용 조회이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • 향후 채널·기기별 설정이 필요해질 때 설정 범위 분리 여부.

notifications

한 줄 역할

냉장고에서 발생한 알림 내용을 수신자 수와 무관하게 한 번 저장한다.

컬럼

2026-09-07 ERD와 동기화한 설계 정의입니다. 운영 DB 적용 완료를 뜻하지 않으며, 미결 사항은 아래에 구분합니다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
notification_id BIGINT N PK 알림 ID - 알림을 식별하는 대리키
type VARCHAR(20) N — 앱 알림함 유형 - EXPIRING_SOON, EXPIRED, MEMBER_JOINED, MEMBER_LEFT, MEMBER_KICKED. 추천 푸시는 저장하지 않음. CHECK로 허용값 강제
title VARCHAR(150) N — 제목 - 발송 시점에 생성한 문구
body VARCHAR(500) N — 본문 - 발송 시점 문구. 대상 재료명·수량 포함
created_at DATETIME(6) N — 생성 일시 - 알림 발송 시각. 정렬 기준 created_at DESC, notification_id DESC
기본값/생성식: CURRENT_TIMESTAMP(6)
updated_at DATETIME(6) N — 수정 일시 - 최종 수정 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
refrigerator_id BIGINT N FK 알림 발생 냉장고 FK. 활성 냉장고별 조회. 예외: MEMBER_KICKED는 퇴출 당사자의 개인 냉장고 ID를 넣는다. 공유 냉장고 ID로 발행하면 접근이 끊긴 당사자에게 조회되지 않기 때문이다

제약조건

  • PK: notification_id
  • FK: refrigerator_id → refrigerators.refrigerator_id
  • UNIQUE: 추가 업무 UNIQUE 없음.
  • CHECK: CHECK (type IN ('EXPIRING_SOON','EXPIRED','MEMBER_JOINED','MEMBER_LEFT','MEMBER_KICKED')) — 알림함에 저장 가능한 유형만 허용. 다섯 값 모두 20자 이내라 VARCHAR(20)을 유지한다

관계

  • refrigerators N:1 — 알림의 조회·권한 범위.
  • notification_recipients 1:0..N — 사용자별 수신·읽음.
  • 개별 대상 연결은 두지 않는다(미채택). 알림 클릭이 재고 목록의 임박·만료 필터 이동으로 확정돼 ingredient 식별자가 필요하지 않다.

생명주기

  • 생성: 임박·만료·구성원 가입·추방 발생 시 본문과 수신자 생성. 추천 푸시는 적재하지 않는다.
  • 변경: 개인별 읽음은 수신자에서 갱신. title/body는 발송 당시 문구를 보존한다.
  • 삭제: push_notifications → notification_recipients → notifications 순으로 정리한다. push_notifications.notification_id가 RESTRICT이므로 발송 작업을 먼저 지워야 한다. 본문을 지우기 전에 다른 수신자의 수신 행이 남아 있는지 확인한다. 정리는 매일 08:00 발행 처리 안에서 함께 수행한다.
  • 보관: 사용자·냉장고 단위로 최대 99건을 유지한다. 알림은 보관 목적의 히스토리가 아니라 해당 상황에 대한 상태 표기이므로, 99건을 넘으면 발행 시점에 삭제한다. 읽은 알림(read_at IS NOT NULL) 중 created_at 오래된 것부터 지우고, 그래도 초과하면 안 읽은 알림 중 오래된 것부터 지운다. 삭제 대상은 notification_recipients 행이며 본문은 수신 행이 하나도 남지 않았을 때만 정리한다. 개인 냉장고와 공유 냉장고의 99건은 따로 센다.

설계 결정

  • 본문과 수신자 읽음 상태를 분리해 동일 문구 중복 저장과 공통 본문의 읽음 충돌을 막는다.
  • title·body는 발송 당시 표현을 보존하는 스냅샷이다. 서로 다른 사건이 같은 문구를 가질 수 있어 UNIQUE를 두지 않는다.
  • 개별 대상 FK와 type+id를 두지 않는 것으로 확정했다. 알림을 탭하면 재고 목록의 해당 상태 필터로 이동할 뿐 특정 재료를 지목하지 않으므로 대상 식별자가 쓰이지 않는다. 다형 참조와 대상별 Nullable FK의 확장 비용을 모두 피한다.
  • 정렬은 created_at DESC, notification_id DESC로 동률을 구분한다. 시간 정밀도만으로 순서를 보장하지 않는다.
  • 재료 알림은 같은 D-day의 재료를 한 건으로 묶는다. 임박이 D-3 이내라 한 건에 몰아넣으면 남은 날이 다른 재료가 섞여 제목과 기한 문구가 성립하지 않으므로, D-0·D-1·D-2·D-3을 각각 별도 알림으로 발행한다. 만료를 포함해 하루 최대 5건이다.
  • title·body는 서버(매일 08:00 발행 배치)가 완성해 저장하고 조회 시 재계산하지 않는다. 같은 문구가 웹 푸시 페이로드에도 쓰이므로 서버가 어차피 생성해야 하며, 프론트가 조립하면 동일 규칙이 서버와 클라이언트 양쪽에 생긴다. 다국어를 도입하면 이 선택을 재검토한다.
  • body 형식은 재료가 1개면 {이름} {수량} · {기한 문구}, 2개 이상이면 {첫 이름} {수량} 외 {N-1}개 · {기한 문구}다. 기한 문구는 오늘까지예요(D-0) / N일 남았어요(D-1~D-3) / N일 지났어요(만료).
  • 내부 PK·참조 FK의 BIGINT는 식별자 타입을 일치시키는 기존 선택이다. DATETIME(6)은 소수초 정밀도를 보존하지만 시간만으로 사건 순서를 보장하지 않는다.
  • type (VARCHAR(20)): EXPIRING_SOON·EXPIRED·MEMBER_JOINED·MEMBER_LEFT·MEMBER_KICKED 코드에 충분한 VARCHAR(20). MEMBER_LEFT는 자발적 나가기와 방장의 내보내기를 함께 표현한다. 남은 구성원 입장에서 둘을 구분할 실익이 없고 누가 퇴출됐는지 드러내지 않는 편이 낫다.
  • title (VARCHAR(150)): 발송 시점의 문구를 보존하고 다국어 확장 여유를 두는 VARCHAR(150).
  • body (VARCHAR(500)): 재료명·수량이 포함된 알림 문구를 한 행에 보존하기 위한 VARCHAR(500).
  • 인덱스: 주요 조회는 사용자·활성 냉장고별 최신 알림과 안정적인 커서 조회이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

결정 완료 (2026-09-07 알림 목록 조회 API 설계)

  • 개별 대상 연결: 미채택. 알림 클릭이 재고 목록 필터 이동이라 불필요.
  • invalidated_at: 미채택. 대상 연결을 두지 않아 처리 여부를 판정할 수단이 없다. 알림을 탭한 뒤 재고 목록이 비어 있으면 재고 화면의 빈 상태로 처리한다.
  • 99건 초과분: 발행 시점 삭제. 읽은 것 → 안 읽은 것 순, 각각 오래된 것부터.
  • 유형: EXPIRING_SOON·EXPIRED·MEMBER_JOINED·MEMBER_LEFT·MEMBER_KICKED 5종.
  • title·body: 서버가 발행 시점에 생성해 저장.

남은 결정

  • 삭제 CASCADE를 DB 제약으로 걸지, 애플리케이션 삭제 순서로만 보장할지.

notification_recipients

한 줄 역할

알림을 받은 회원과 그 회원이 읽은 시각을 저장한다.

컬럼

2026-09-07 ERD와 동기화한 설계 정의입니다. 운영 DB 적용 완료를 뜻하지 않으며, 미결 사항은 아래에 구분합니다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
notification_recipient_id BIGINT N PK 수신 ID - 대리키
read_at DATETIME(6) Y — 읽은 일시 - 안 읽었으면 NULL (개인별 읽음)
user_id BIGINT N FK 회원 ID - 회원을 식별하는 내부 대리키
notification_id BIGINT N FK 실제 수신 알림. UNIQUE(user_id, notification_id)

제약조건

  • PK: notification_recipient_id
  • FK: user_id → users.user_id; notification_id → notifications.notification_id
  • UNIQUE: UNIQUE (user_id, notification_id) — 동일 회원에게 같은 알림 중복 생성 차단
  • CHECK: 추가 CHECK 정의 없음.

관계

  • users N:1; notifications N:1 — 회원과 알림의 N:M 연결.

생명주기

  • 생성: 발행 시 수신 대상에게 생성; 미로그인 회원도 read_at=NULL로 적재. 공유 가입 이전 알림은 소급 생성하지 않음.
  • 변경: 읽으면 read_at 갱신. 전체 읽음은 현재 사용자·활성 냉장고 범위로 단일 UPDATE하되, 요청 시작 시각으로 경계를 둔다(AND n.created_at <= :requestedAt). 요청 처리 도중 발행된 알림까지 읽음 처리되어 사용자가 본 적 없는 알림이 사라지는 것을 막는다. 필터가 걸려 있어도 전체 알림을 대상으로 한다.
  • 삭제: 알림 본문 정리 또는 보관 정책에 따라 종속 행 제거.
  • 보관: 냉장고 전환은 노출 범위 변경이며 숨긴 개인 알림을 삭제하지 않는다. 99건 상한은 사용자·냉장고 단위로 적용하므로 개인 냉장고와 공유 냉장고를 따로 센다. 초과분은 발행 시점에 삭제하며, 삭제 단위는 본문이 아니라 이 테이블의 수신 행이다. 공유 냉장고에서 한 수신자만 99건을 넘었을 때 본문을 지우면 다른 수신자의 알림까지 사라지기 때문이다.

설계 결정

  • 읽음은 회원마다 다르므로 notifications 공통 본문에 저장하지 않는다.
  • 대리 PK는 수신 행 식별, (user_id, notification_id) UNIQUE는 중복 전달 행 방지에 사용한다.
  • read_at NULL 자체가 미읽음 상태라 별도 is_read와의 불일치가 없다.
  • 확장: 채널별 전달 성공·실패 추적이 필요하면 수신·읽음 모델과 발송 시도 이력을 분리한다.
  • 내부 PK·참조 FK의 BIGINT는 식별자 타입을 일치시키는 기존 선택이다. DATETIME(6)은 소수초 정밀도를 보존하지만 시간만으로 사건 순서를 보장하지 않는다.
  • 인덱스: 주요 조회는 사용자별 미읽음과 활성 냉장고 알림 조인이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

결정 완료 (2026-09-07 알림 목록 조회 API 설계)

  • 전체 읽음은 요청 시작 시각으로 경계를 둔다.
  • 보관은 사용자·냉장고당 99건이며 초과분은 발행 시점에 수신 행을 삭제한다. 읽은 행 → 안 읽은 행 순, 각각 오래된 것부터.
  • 삭제 순서는 push_notifications → notification_recipients → notifications다.

남은 결정

  • 연쇄 삭제를 DB CASCADE로 걸지, 애플리케이션 삭제 순서로만 보장할지.

user_devices

한 줄 역할

브라우저별 웹 푸시 구독과 현재 소유자·키 버전을 저장한다.

동기화 상태

2026-09-07 ERD 구조 반영. 실제 운영 DB·발송 기능 구현 완료를 뜻하지 않는다.

컬럼과 처리 규칙

원문 명칭을 유지한다. ‘기기 전체 정보’가 아니라 구독임을 역할에 명시한다.

물리명 타입 Null 키 설명
user_device_id BIGINT N PK 브라우저 푸시 구독 식별자. 물리 기기 전체 정보가 아님
endpoint VARCHAR(2048) N — 푸시 서비스 URL. UNIQUE. 대소문자 보존. 16KB InnoDB DYNAMIC 전제
p256dh_key VARCHAR(128) N — 구독 공개키 Base64URL
auth_secret_encrypted BLOB N — 구독 auth 암호문 nonce 인증태그. 실제 암호화 키는 외부 보관
encryption_key_version VARCHAR(64) N — 저장 암호화 키 버전
vapid_key_version VARCHAR(64) N — 구독 생성에 사용한 서버 VAPID 공개키 버전
subscription_version BIGINT N — 소유자 키 활성화 변경 시 증가. CHECK 0 초과
기본값/생성식: 1
status VARCHAR(16) N — ACTIVE / DISABLED / INVALID. 로그아웃은 해당 구독 비활성
기본값/생성식: 'ACTIVE'
created_at DATETIME(6) N — 최초 구독 등록 시각
updated_at DATETIME(6) N — 최종 갱신 시각
user_id BIGINT N FK 현재 구독 소유 회원 FK. NOT UNIQUE. 삭제 RESTRICT
  • 등록은 로그인 사용자 기준으로 처리하며 클라이언트가 보낸 user_id를 신뢰하지 않는다. endpoint 등록만으로 다른 계정 소유 구독을 임의 이전하지 않는다.
  • 로그아웃은 해당 브라우저 구독을 DISABLED로 바꾸고 버전을 증가시킨다. 다른 기기까지 해제하지 않는다. 재로그인·계정 전환은 검증된 재연결 후 새 버전을 사용한다.
  • 발송 직전에 구독 상태·user_id·subscription_version·알림 수신 설정·활성 냉장고 접근권한을 재검증한다. 이미 푸시 서비스가 수락한 메시지는 로그아웃 시 회수된다고 보장할 수 없다. 잠금화면용 payload에는 민감정보를 최소화한다.
  • 탈퇴 시 구독 즉시 비활성화·대기 작업 취소. 실제 파기는 발송 로그 보관정책과 함께 처리한다. 운영 보관기간은 아직 미정이다.
  • endpoint도 민감한 구독 정보다. 로그 마스킹·DB 접근 제한, HTTPS 및 지원 푸시 서비스 URL 검증·내부망 요청 차단을 적용한다.
  • 개발 .env는 Git 제외·접근 제한 조건에서 사용한다. 운영은 Secret Manager 등으로 키를 주입하며 .env 파일만으로 안전한 키 관리가 완성됐다고 하지 않는다. 키는 저장소·이미지·프런트엔드에 넣지 않는다.

근거

문자셋·인덱스: endpoint는 ASCII/ascii_bin으로 저장하고 UNIQUE를 적용한다. 16KB InnoDB·DYNAMIC 및 최대 2048자 조건을 배포 환경에서 확인한다. p256dh_key는 ASCII다.


push_notifications

한 줄 역할

구독별 논리 발송 작업·상태·재시도을 저장한다.

동기화 상태

2026-09-07 ERD 구조 반영. 실제 운영 DB·발송 기능 구현 완료를 뜻하지 않는다.

컬럼과 처리 규칙

한 행 = 하나의 논리 발송 이벤트를 하나의 구독 연결 버전에 발송하는 작업. 같은 작업의 재시도는 같은 행을 사용한다. 알림함과 발송 로그는 다른 저장 목적이다.

물리명 타입 Null 키 설명
push_notification_id BIGINT N PK 구독별 논리 발송 작업 ID. 재시도는 같은 행
dispatch_key VARCHAR(191) N — 논리 이벤트 키. UNIQUE(dispatch_key,user_device_id,subscription_version)
kind VARCHAR(20) N — INBOX / RECOMMENDATION
user_device_id BIGINT N FK 발송 대상 구독 FK
subscription_version BIGINT N — 생성 당시 구독 버전. CHECK 0 초과
payload JSON N — 발송 문구 및 내부 경로 스냅샷. 재시도로 추천 재생성하지 않음
status VARCHAR(16) N — PENDING / PROCESSING / RETRY / ACCEPTED / FAILED / CANCELLED
기본값/생성식: 'PENDING'
attempt_count INT N — 실제 시도 수. CHECK 0 이상
기본값/생성식: 0
next_attempt_at DATETIME(6) Y — PENDING RETRY는 필수. 최초 예정 또는 재시도 시각
lease_until DATETIME(6) Y — PROCESSING이면 필수. 작업 점유 만료
claim_token VARCHAR(64) Y — PROCESSING이면 필수. 늦은 worker 결과 갱신 차단
expires_at DATETIME(6) N — 발송 유효 기한. 경과 시 재시도 금지
accepted_at DATETIME(6) Y — ACCEPTED이면 필수, 그 외 NULL. 푸시 서비스 수락이며 읽음 아님
last_error_code VARCHAR(64) Y — 오류 분류. 구독 URL 비밀키 원문 기록 금지
created_at DATETIME(6) N — 작업 생성 시각
updated_at DATETIME(6) N — 작업 최종 갱신
notification_id BIGINT Y FK 알림 ID - 알림을 식별하는 대리키
user_id BIGINT N FK 작업 생성 당시 FK. 발송 전 수신자 구독 소유자와 냉장고 접근권한 재검증. 삭제 RESTRICT
refrigerator_id BIGINT N FK 작업 생성 당시 FK. 발송 전 수신자 구독 소유자와 냉장고 접근권한 재검증. 삭제 RESTRICT

제약·처리 규칙

  • UNIQUE(dispatch_key, user_device_id, subscription_version). dispatch_key는 INBOX의 알림 ID 또는 추천의 발송일·회원·냉장고를 포함해 결정적으로 생성한다. 같은 오전 발송의 추천 내용은 첫 작업 생성 시 고정하고 재시도 시 재선택하지 않는다.
  • CHECK((kind='INBOX' AND notification_id IS NOT NULL) OR (kind='RECOMMENDATION' AND notification_id IS NULL)).
  • 이 Nullable FK 하나는 ‘알림함 이벤트와 연결되는가’라는 선택 관계다. 대상 유형마다 FK가 늘어나는 다형 참조 구조와 다르다.
  • INBOX 발송 시 notification_recipients에 실제 수신자가 있고 냉장고가 일치하는지 검증한다. 기존 수신 설정에서 승인된 유형만 발송한다. 다만 구성원 알림(MEMBER_JOINED·MEMBER_LEFT·MEMBER_KICKED)은 notification_preferences의 on/off 대상이 아니며, 특히 MEMBER_KICKED는 당사자가 상황을 알 유일한 수단이므로 항상 발송한다. 세 유형 모두 알림함에 남으므로 kind는 INBOX이며 별도 kind 값을 추가하지 않는다.
  • 앱 알림함은 notifications + notification_recipients를 조회한다. push_notifications를 합쳐 표시하지 않는다. 추천 푸시는 두 알림함 테이블에 행을 만들지 않는다.
  • ACCEPTED이면 accepted_at 필수, 그 외에는 NULL. PROCESSING이면 claim_token·lease_until 필수. PENDING/RETRY면 next_attempt_at 필수. 이 상태 조합은 CHECK와 worker 전이 테스트로 강제한다.
  • DB 원자적 선점으로 한 작업을 점유하고 결과 갱신은 claim_token이 일치할 때만 수행한다. 만료 임대는 재회수한다. 네트워크 발송 중 DB 트랜잭션을 계속 잡아두지 않는다.
  • 일시 장애·호출 제한은 제한된 backoff로 같은 행을 재시도한다. Retry-After를 존중한다. 구독 무효 응답은 INVALID로 전환하고 해당 대기 작업을 취소한다. 인증 오류는 VAPID 설정 오류와 구독 만료를 구분한다.
  • UNIQUE와 선점은 작업 중복을 줄일 뿐 외부 전송의 exactly-once를 보장하지 않는다. 수락 응답 유실 시 재전송될 수 있다. 클라이언트의 동일 이벤트 tag 등 표시 중복 완화도 검토한다.
  • FK 삭제는 기본 RESTRICT로 정의하고, 보관기간이 끝난 발송 작업 정리 후 부모를 파기한다. 알림함은 사용자·냉장고당 99건을 넘으면 발행 시점에 삭제한다. notification_id FK가 RESTRICT이므로 알림 본문을 지우려면 해당 발송 작업 행을 먼저 정리해야 한다. 삭제 순서는 push_notifications → notification_recipients → notifications다.
  • 최대 재시도 횟수·backoff·유효 기한·로그 보관기간·worker 임대 시간은 운영 결정 항목이다. 임의 수치를 정책으로 확정하지 않는다.

기존 추천 정책과의 경계

07:55 준비에도 냉장고당 일일 생성 10회 제한을 적용한다. 발송 재시도는 추천을 다시 생성하거나 순번을 다시 증가시키지 않는다. 새 추천 준비 실패·한도 도달 시 기존 결과를 발송할지 건너뛸지는 별도 확정이 필요하다. 현재 인메모리 캐시·생성 횟수의 재시작 초기화 한계를 이 발송 테이블만으로 해결했다고 하지 않는다.

근거

문자 비교: dispatch_key는 ASCII/ascii_bin, claim_token은 ASCII로 저장한다. UNIQUE(dispatch_key, user_device_id, subscription_version)를 적용한다. 모든 부모 FK의 삭제 동작은 RESTRICT이며 보관 종료 작업을 먼저 정리한다.


recipes

한 줄 역할

AI가 제공하는 레시피를 백엔드 조회용 사본으로 저장한다.

컬럼

2026-09-07 레시피 사본 구조를 ERD에 반영했다. 운영 동기화는 AI 계약 합의 후 구현한다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
recipe_id BIGINT N PK 백엔드 레시피 사본 ID
source VARCHAR(32) N — 원천 구분. UNIQUE(source, source_recipe_id)
source_recipe_id VARCHAR(191) N — 원천 안정 식별자. 대소문자 구분
source_revision VARCHAR(128) N — 원천 버전 토큰. 문자열 대소 비교로 최신 순서 판단 금지
name VARCHAR(255) N — 레시피 이름
image_url VARCHAR(2048) Y — 선택 이미지 URL
base_servings DECIMAL(10, 3) Y — 기준 인분. CHECK NULL 또는 0 초과
status VARCHAR(16) N — ACTIVE / INACTIVE. 원천 삭제는 INACTIVE로 반영
synced_at DATETIME(6) N — 마지막 성공 적재 시각. 부모 재료 단계는 원자적 반영

제약조건

문자 비교: source_recipe_id는 utf8mb4_bin으로 대소문자를 구분한다. UNIQUE(source, source_recipe_id)를 실제 DDL에 적용한다.

  • PK: recipe_id — 외부 식별 체계와 독립된 내부 참조.
  • FK: 이 테이블에서 나가는 FK 없음.
  • UNIQUE: (source, source_recipe_id) — 동일 원천 레시피 중복 적재 방지.
  • CHECK: base_servings IS NULL OR base_servings > 0; status IN ('ACTIVE','INACTIVE').

관계

  • recipe_ingredients, recipe_steps 각각 1:N — 정상 공개 레시피에 각각 최소 1개 필요.
  • [검토안] cooking_records 1:0..N — 조리 기록에서 선택 참조.

생명주기

  • 생성: 최초 전체 적재 후 주기적 증분 API로 생성.
  • 변경: 원천 키로 멱등 upsert; 역순 버전 차단. 본문·재료·단계는 하나의 트랜잭션으로 갱신.
  • 삭제: 명시적 삭제 이벤트 또는 완전한 전체 스냅샷 대조로 INACTIVE 처리. API 장애만으로 삭제하지 않음.
  • 보관: 실패 시 마지막 정상 사본 제공. 조리 기록이 참조하면 물리 삭제 RESTRICT 제안.

설계 결정

  • AI는 수집·정규화·추천 알고리즘, 백엔드는 서비스 조회·권한·차감을 담당한다. API 매핑 계층으로 원천 스키마와 내부 모델을 연결한다.
  • 레시피 사본 테이블 구조를 설계에 반영했다. 운영 동기화는 원천 계약 합의 이후이며 지연·오류·운영 비용을 함께 관리한다.
  • 자식 재료·단계는 반복 집합이라 별도 테이블로 분리하고 추천 순위·보유량·부족량은 냉장고별 캐시에 둔다.
  • 확장: source 복합 식별로 여러 원천을 구분한다. 커서·마지막 성공 시각은 내구성 있는 작업 저장소에 보존한다.
  • 타입·길이는 PL 결정 기록의 부록 C와 ERD에 맞췄다. 운영 적용 전 AI 응답·단위·버전 계약으로 검증한다.
  • 인덱스: 주요 조회는 원천 복합 키 upsert 및 recipe_id 상세 조회이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • 안정 ID·revision 순서·단위·인분·삭제 표시·증분 커서·페이지 계약. 현재 설계 타입/길이는 실제 AI 응답으로 검증한다.
  • 동기화 주기·허용 지연·재시도와 커서 저장 위치. 부분 실패 시 마지막 성공 위치부터 재시도.
  • ID 없는 생성형 레시피 지원 여부와 추천 캐시 매핑 계약.

recipe_steps

한 줄 역할

레시피별 순서가 있는 조리 설명과 선택 이미지를 저장한다.

컬럼

2026-09-07 ERD에 구조를 반영했다. 운영 동기화는 AI 계약 합의 후 구현한다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
recipe_step_id BIGINT N PK 레시피 단계 행의 단일 대리키. 조합 중복은 UNIQUE(recipe_id, step_no)로 방지
recipe_id BIGINT N FK 백엔드 레시피 사본 ID
step_no INT N — CHECK step_no > 0. UNIQUE(recipe_id, step_no). 조회 시 ORDER BY step_no
instruction TEXT N — 조리 설명
image_url VARCHAR(2048) Y — 선택 단계 이미지

제약조건

  • PK: recipe_step_id — BIGINT 단일 대리키로 행 식별.
  • FK: recipe_id → recipes.recipe_id.
  • UNIQUE: (recipe_id, step_no) — 같은 레시피 안의 번호 중복 방지.
  • CHECK: step_no > 0. 순번 연속성·최소 한 단계는 동기화 응답 검증에서 확인한다.

관계

  • recipes N:1 — 각 단계는 한 레시피에 종속.

생명주기

  • 생성: 검증된 레시피를 부모·재료와 함께 적재.
  • 변경: revision 변경 시 단계 집합을 트랜잭션으로 교체; 한 단계 실패 시 전체 반영 실패.
  • 삭제: 부모 물리 삭제가 허용되면 종속 삭제.
  • 보관: 마지막 정상 사본 유지; 공개 레시피는 최소 한 단계 필요.

설계 결정

  • 순서 있는 반복 집합을 별도 테이블로 분리해 본문에 단계별 컬럼을 늘리지 않는다.
  • recipe_step_id를 단일 PK로 두고 (recipe_id, step_no)는 UNIQUE로 관리한다. FK 참조·ORM 식별자·API 리소스 표현을 단순화하는 프로젝트 선택이다. 복합 PK 자체를 안티패턴으로 규정하지 않는다. recipe_id는 PK에 포함하지 않는 일반 FK이며, 번호 변경 시 식별자와 구분한다.
  • 확장: 단계 이미지가 선택값이므로 텍스트만 있는 레시피도 표현한다. 영상 등 새 요구는 원천 계약 확인 후 확장한다.
  • 타입·길이는 PL 결정 기록의 부록 C와 ERD에 맞췄다. 운영 적용 전 AI 응답·단위·버전 계약으로 검증한다.
  • 인덱스: 주요 조회는 recipe_id별 단계 목록을 step_no 순으로 조회이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • step_no는 양수 INT, instruction은 TEXT, image_url은 nullable VARCHAR(2048)로 설계 반영. 순번 안정성·재정렬 계약을 확인한다.
  • 부모 물리 삭제 시 자식 CASCADE로 설계한다. 운영 동기화 실패·재시도를 검증한다.

recipe_ingredients

한 줄 역할

레시피별 필요 재료와 원천 수량 표현을 저장한다.

컬럼

2026-09-07 ERD에 구조를 반영했다. 운영 동기화는 AI 계약 합의 후 구현한다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
recipe_ingredient_id BIGINT N PK 레시피 재료 행의 단일 대리키. 조합 중복은 UNIQUE(recipe_id, ingredient_no)로 방지
recipe_id BIGINT N FK 백엔드 레시피 사본 ID
ingredient_no INT N — CHECK ingredient_no > 0. UNIQUE(recipe_id, ingredient_no). 재료명 중복 여부와 별개
name VARCHAR(255) N — 원천 재료명
amount_text VARCHAR(255) Y — 소금 약간 등 원천 필요량 표현
measure_type VARCHAR(16) N — COUNT / WEIGHT / VOLUME / UNKNOWN. 단위와 수치 CHECK 필요
amount_value DECIMAL(10, 3) Y — 확인된 필요량만 수치화. UNKNOWN은 NULL. COUNT는 양수 정수
unit VARCHAR(8) Y — COUNT=EA, WEIGHT=G, VOLUME=ML, UNKNOWN=NULL

제약조건

  • PK: recipe_ingredient_id — BIGINT 단일 대리키로 행 식별.
  • FK: recipe_id → recipes.recipe_id. ingredients에 직접 FK 없음.
  • UNIQUE: (recipe_id, ingredient_no) — 같은 레시피 안의 번호 중복 방지.
  • CHECK: ingredient_no > 0. UNKNOWN은 값·단위 NULL; COUNT는 양수 정수·EA; WEIGHT는 양수·G; VOLUME은 양수·ML. 수치형 분기는 값·단위 IS NOT NULL을 함께 검사한다.

관계

  • recipes N:1.
  • 사용자 재고 ingredients와 직접 관계 없음; 조리 요청에서 현재 냉장고 재고와 검증·매칭.

생명주기

  • 생성: 레시피 전체 응답 검증 후 부모·단계와 함께 적재.
  • 변경: 레시피 버전 갱신 시 같은 트랜잭션으로 자식 집합 교체.
  • 삭제: 부모의 물리 삭제가 허용될 때 종속 삭제; 원천 비활성화만으로 강제 파기하지 않음.
  • 보관: 마지막 정상 사본 보관. 과거 추천의 필요량·버전은 추천 생성 시 별도 보존.

설계 결정

  • 레시피 재료는 여러 냉장고에서 사용하는 요구량이며 재고는 유통기한·보관 방식별 실물 행이다. 둘을 직접 FK로 연결하면 레시피가 특정 재고에 종속된다.
  • recipe_ingredient_id를 단일 PK로 두고 (recipe_id, ingredient_no)는 UNIQUE로 관리한다. FK 참조·ORM 식별자·API 리소스 표현을 단순화하는 프로젝트 선택이다. 복합 PK 자체를 안티패턴으로 규정하지 않는다. recipe_id는 PK에 포함하지 않는 일반 FK이며, 번호 변경 시 식별자와 구분한다.
  • DECIMAL은 수치량의 십진 표현을 위한 제안이다. 약간·한 줌을 임의 수치로 바꾸지 않고 검증된 변환 또는 사용자 확인 없이는 자동 차감을 막는다.
  • 확장: 검증된 단위 변환이 필요해지면 사전·변환 규칙을 도입한다. 최신 사본으로 과거 추천의 차감량을 조용히 바꾸지 않는다.
  • 타입·길이는 PL 결정 기록의 부록 C와 ERD에 맞췄다. 운영 적용 전 AI 응답·단위·버전 계약으로 검증한다.
  • 인덱스: 주요 조회는 recipe_id별 필요 재료 조회이다. PK·UNIQUE와 FK 지원 인덱스를 확인하고, 추가 조회 인덱스는 실제 쿼리·데이터량·실행 계획으로 결정한다. 문서만으로 구현 완료로 간주하지 않는다.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • 설계는 COUNT/WEIGHT/VOLUME/UNKNOWN, DECIMAL(10,3), amount_text NULL 허용으로 반영했다. AI 데이터의 범위·변환 가능성을 검증한다.
  • 재료명 매칭과 모호한 필요량의 사용자 확인 흐름.
  • 동일 재료 반복과 ingredient_no 안정성 계약.

인메모리 캐시에 저장될 데이터

in_memory_recommendation_cache

한 줄 역할

현재 재고에 대응하는 추천 결과·일일 생성 횟수·실행 중 생성 작업을 프로세스 메모리에 보관한다.

컬럼

인메모리 논리 구조. 표의 타입은 설명용 표기다.

Null: N=불가, Y=허용, 미정=합의 필요. 기본값은 설명 열에 표기한다.

물리명 타입 Null 키 설명
cache_key VARCHAR(255) N PK 애플리케이션 인스턴스 내부 추천 캐시 키. refrigeratorId 기준
refrigerator_id BIGINT N — 캐시 파티션 기준 냉장고 ID. MySQL FK가 아닌 논리 참조
inventory_fingerprint VARCHAR(64) N — 추천 생성 당시 재고 상태 해시. SHA-256 16진수 64자
recommendation_id VARCHAR(64) N — 추천 목록 식별자
recipes_json JSON N — 요리·필요 재료·수량·상세를 포함한 추천 결과
generated_at DATETIME(6) N — AI 추천 생성 완료 시각
recommendation_mode VARCHAR(20) N — 만료 재료 INCLUDE_EXPIRED / EXCLUDE_EXPIRED 구분
push_cursor SMALLINT UNSIGNED N — 인메모리 캐시에 보관하는 다음 추천 푸시 순번
기본값/생성식: 0
daily_generation_count TINYINT UNSIGNED N — ConcurrentHashMap<(refrigeratorId, count_date), AtomicInteger>의 값. 원자적 증가 후 10 초과 요청 거부. 프로세스 재시작 시 초기화
기본값/생성식: 0
count_date DATE N — 일일 생성 횟수의 날짜 범위. 날짜 변경 시 이전 인메모리 카운터 정리
generation_task_key VARCHAR(255) Y — (refrigeratorId, inventory_fingerprint) 조합. ConcurrentHashMap.computeIfAbsent로 동일 생성 요청이 하나의 CompletableFuture를 공유

제약조건

  • PK: 해당 없음 — 메모리 cache_key 사용.
  • FK: 해당 없음 — refrigerator_id 논리 참조.
  • UNIQUE: DB 제약 없음 — 생성 작업 키별 공유 Future로 중복 호출 제어.
  • CHECK: DB 제약 없음 — 하루 최대 10회 원자 카운터, 유효한 모드·푸시 순번은 앱에서 검증.

관계

  • refrigerators N:1 논리 참조 — 모드·재고 버전별 추천을 구분; 물리 FK 없음.
  • [검토안] recipes 원천 ID·revision 매핑; DB FK 없음.

생명주기

  • 생성: 진입 시 일치 결과가 없거나 07:55 사전 준비 시 일일 한도 내 생성.
  • 변경: 기존 결과는 먼저 표시하고 새 결과는 다음 진입부터 적용. 새 목록은 푸시 순번 초기화; 08:00 순차 푸시.
  • 삭제: TTL·용량·프로세스 종료, 날짜 변경 카운터 정리, 완료·실패 Future 제거; 탈퇴·접근 종료 시 퇴출.
  • 보관: 단일 프로세스 임시 상태. 재시작 시 결과·한도·푸시 순번이 사라지고 서버끼리 공유하지 않음.

설계 결정

  • 추천 JSON은 냉장고·재고·모드별 계산 결과라 공통 레시피 사본과 역할이 다르다.
  • ConcurrentHashMap.computeIfAbsent와 공유 CompletableFuture로 동일 생성 작업을 묶는다. 단순 불리언 조회 후 설정으로 대체하지 않는다.
  • 일일 카운터는 날짜·냉장고별 AtomicInteger로 제어한다. 캐시 재사용·정렬·기존 결과 푸시는 생성 횟수에 포함하지 않는다.
  • 확장: 다중 서버·재시작 후 한도 유지·중복 생성 방지가 필요해지면 Redis 등의 공유 저장소와 분산 조정을 검토한다.
  • 표의 SQL 타입은 기존 ERD의 논리 표기이며 실제 자바 필드나 MySQL DDL이 아니다.
  • cache_key (VARCHAR(255)): 냉장고 기준 캐시 키를 표현하며 로컬 Map의 키로 사용한다.
  • inventory_fingerprint (VARCHAR(64)): SHA-256 16진수 64자를 저장한다.
  • recommendation_id (VARCHAR(64)): UUID/해시 기반 식별자에 맞춘 VARCHAR(64).
  • recipes_json (JSON): 중첩된 추천 결과 전체를 메모리에 보관하는 JSON 표현이다.
  • recommendation_mode (VARCHAR(20)): INCLUDE_EXPIRED·EXCLUDE_EXPIRED 코드에 충분한 VARCHAR(20).
  • push_cursor (SMALLINT UNSIGNED): 작은 추천 목록 순번이라 SMALLINT UNSIGNED면 충분하다.
  • daily_generation_count (TINYINT UNSIGNED): 0~10 범위의 로컬 원자 카운터라 TINYINT UNSIGNED 표기로 충분하다.
  • count_date (DATE): 카운터의 일자 범위를 명시하고 날짜 변경 시 정리한다.
  • generation_task_key (VARCHAR(255)): 냉장고 ID와 64자 fingerprint 조합에 여유 있는 VARCHAR(255) 논리 키다.
  • 인덱스: DB 인덱스 해당 없음; 메모리 Map 키 설계.
  • 컬럼 순서: 기존 표의 순서를 유지한다. 읽기 편의를 위한 표시 순서이며 조회 성능의 근거로 삼지 않는다. 복합키·인덱스의 컬럼 순서는 별도 판단이다.

남은 결정

  • 캐시 키·생성 작업 키에 냉장고 ID, 재고 지문, 만료 포함 모드를 일관되게 반영할 구현 계약.
  • TTL·최대 용량·실패 시 카운터 처리와 기준 시간대.
  • 원천 ID·revision 매핑 도입 시 캐시와 레시피 사본 버전 차이 처리.