1. 왜 이 글을 쓰게 되었는가
https://wo-dbs.tistory.com/381
[외주] 기획적으로 풀기 어려운 요구사항 정리 및 기능별 화면명세서 - 4/3 작성 - 3
이번에는 기획적으로 풀기 어려운 요구사항을 정리하고 각 시나리오에 대해 작성해서 사장님과 미팅을 진행해보려고 한다. 4.2 입실자 관리4.2.1 입실자 목록 조회 → 예정 입실 기능사용자는 입
wo-dbs.tistory.com
위 글에서는 예정 입실 기능이 왜 기획적으로 애매한지, 어떤 운영 시나리오가 있는지 중심으로 정리했다. 그런데 요구사항을 정리하는 것만으로는 실제 구현 과정이 잘 드러나지 않는다고 느꼈다. 이번 글에서는 그 후속편으로, 예정 입실 기능을 실제로 어떤 기준으로 해석했고 어떤 데이터로 분리했으며, 조회는 어떤 방식으로 풀어가려고 했는지를 정리해보려고 한다.
2. 처음 받은 요구사항
처음 받은 요구사항의 핵심은 단순했다. 현재 공실 여부만으로는 객실 운영 판단이 어렵기 때문에, 앞으로 예정된 입실 일정까지 함께 조회할 수 있어야 한다는 것이었다.
예를 들어 오늘이 3월 24일인데, 어떤 방에는 3월 26일부터 입실하기로 한 사람이 이미 정해져 있을 수 있다. 그런데 그 사이에 다른 사람이 3월 27일부터 입실 가능한지 문의할 수도 있다. 이런 상황에서는 단순히 지금 공실인지 아닌지만 보는 것으로는 부족하다. 운영자 입장에서는 이 방이 현재 비어 있는지뿐 아니라, 앞으로 이미 예약된 입실 일정이 있는지도 함께 알아야 하기 때문이다. 또한, 고시원이라 계약서에 계약일과 입실일이 나뉘어서 들어가기 때문이다.
처음에는 이 요구사항이 단순히 “예정 입실일 하나를 더 보여주면 되는 기능”처럼 보였다. 하지만 조금만 더 들여다보니, 이 기능은 단순한 날짜 표시 기능으로 보기 어려웠다. 실제 운영에서는 계약일과 입실일이 다르게 쓰이고 있었고, 월세 납부 기준도 입실일을 따라간다는 점이 있었기 때문이다.
즉 예정 입실 기능은 단순히 미래 날짜를 하나 저장하는 문제가 아니라, 계약 기준 정보와 실제 운영 기준 정보를 함께 다뤄야 하는 기능으로 보기 시작했다.
3. 운영에서는 실제로 어떤 일이 일어나는가
이 기능을 이해하기 위해서는 먼저 실제 운영 흐름을 봐야 했다. 총무님의 업무 흐름은 대략 다음과 같았다.
[실제 총무님이 말씀해주신 시나리오]
- 입실자는 고시원과 계약을 진행하고 계약서를 작성한다.
- 총무님은 계약서에 계약일을 기록한다.
- 이후 입실자가 계약일보다 늦은 날짜에 실제로 들어오겠다고 연락하는 경우가 있다.
- 총무님은 이를 다시 확인한 뒤, 계약서에 실제 입실일을 따로 적는다.
- 이후 프로그램에도 그에 해당하는 내용을 반영한다.
이 흐름을 보면서 가장 먼저 들었던 생각은, 계약일과 입실일은 같은 의미의 데이터가 아니다는 점이었다.
계약일은 계약이 이루어진 기준 날짜다. 반면 입실일은 실제로 방 사용이 시작되는 날짜다. 그리고 이 차이는 단순히 기록상의 차이에 그치지 않았다. 예를 들어 계약일이 3월 24일로 잡혀 있더라도, 월세 납부 기준일은 계약일이 아니라 실제 입실일을 기준으로 정해졌다. 즉 계약은 먼저 이루어질 수 있지만, 실제 운영과 금전 정산은 입실일을 중심으로 돌아가고 있었다.
이 지점에서 예정 입실 기능은 단순한 “미래 일정 표시”가 아니라, 계약과 실제 운영이 어긋날 수 있는 상황을 다루는 기능처럼 보이기 시작했다.
4. 기획 요구사항을 구현으로 옮기려 하니 생긴 질문들
운영 시나리오를 바탕으로 요구사항을 다시 보니, 구현 단계에서는 몇 가지 질문이 자연스럽게 생겼다.
- 계약일과 입실일을 같은 값으로 봐도 되는가
- 예정 입실 정보는 현재 입실자 데이터에 붙여도 되는가
- 방의 공실 여부는 계약일 기준으로 판단해야 하는가, 입실일 기준으로 판단해야 하는가
- 월세 납부 기준처럼 실제 운영은 입실일을 따라가는데, 이 기준을 시스템에서도 분리해서 가져가야 하는가
- 예정 입실이 취소되거나 변경되면 어떻게 관리해야 하는가
- 예정 입실은 한 방에 하나만 둘 수 있는가, 아니면 여러 건까지 허용해야 하는가
- 목록 화면과 상세 화면에서 같은 깊이로 보여줘야 하는가
- 계약상 입실일과 실제 예정 입실일이 다른데 둘 다 저장해야 하나
- 예정 입실이 방의 공실 여부에 영향을 주는가
- 방에 이미 입실자가 있을 때와 입실자가 없는 상황일 때 어떻게 데이터를 저장해야 하나
- 예정 입실이 취소되거나 밀리면 이력을 남겨야 하나 → 취소 및 밀리는 것에 대한 이력
- 예정 입실일에 대한 모든 이력을 남겨야하나 or 이력을 안 남겨도 되는가
처음에는 화면에서 보여줄 항목만 정리하면 되는 문제처럼 보였지만, 실제로는 그렇지 않았다.
이 기능을 제대로 구현하려면 먼저 어떤 데이터를 어떤 의미로 나눠서 저장할 것인가부터 정리되어야 했다. 그래야 공실 여부도 정확히 판단할 수 있고, 월세나 계약 정보처럼 다른 기능과도 충돌하지 않는 구조를 만들 수 있기 때문이다.
이 시점부터 예정 입실 기능은 단순한 UI 문제가 아니라, 도메인을 어떻게 해석할 것인가의 문제로 보이기 시작했다.
5. 처음에는 입실자 데이터에 붙이면 된다고 생각했다
처음 내가 생각한 방식은 비교적 단순했다. 입실자 목록 화면에 예정 입실 컬럼을 두고, 각 방마다 예정 입실이 있는지 없는지를 표시하는 방식이었다.
즉 구조는 대략 이런 느낌이었다.
- 입실자 목록에서 현재 입실 상태를 보여준다.
- 각 방 옆에 예정 입실 있음/없음을 표시한다.
- 예정 입실이 있는 경우 상세 화면에서 다음 입실자의 정보를 보여준다.
- 이때 계약일과 실제 입실일도 함께 볼 수 있도록 한다.
처음에는 이 방식이 꽤 괜찮아 보였다. 운영자 입장에서는 입실자 목록에서 현재 상태와 예정 여부를 한 번에 볼 수 있기 때문이다. 화면도 복잡하지 않고, 기능도 비교적 빠르게 구현할 수 있을 것 같았다.
하지만 막상 이 구조를 화면으로 그리고, 실제 운영 흐름과 연결해보니 점점 아쉬운 점이 보였다. 다음 사진은 원래 했던 화면이다.(실제 데이터는 아니다. 가짜 데이터이다.)


6. 그런데 이 방식이 부족해 보였다
가장 먼저 느낀 한계는, 예정 입실이 단순히 “다음 사람의 정보” 정도로 취급된다는 점이었다.
예정 입실 있음이라는 표시만으로는 실제 운영에 필요한 정보가 충분히 드러나지 않았다. 예를 들어 운영자 입장에서는 단순히 예정 입실 여부만 아는 것이 아니라,
- 누구의 예약인지
- 계약은 언제 이루어졌는지
- 실제 입실은 언제인지
- 월세 납부 기준은 언제부터 잡히는지
- 현재 입실자 퇴실과 얼마나 이어지는지
- 퇴실일에 대한 기준
- 이 일정이 취소되거나 변경될 가능성은 없는지
같은 정보가 함께 중요했다.
또 하나의 문제는 현재 입실자와 예정 입실자가 같은 단위 안에서 섞인다는 점이었다. 현재 입실자는 이미 거주 중인 상태를 나타내고, 예정 입실은 아직 발생하지 않은 미래 상태를 나타낸다. 그런데 이를 같은 레벨에서 다루면 현재 상태와 미래 상태의 경계가 흐려질 수 있다고 느꼈다.
특히 월세 납부 기준이 실제 입실일을 따라간다는 점까지 생각해보면, 예정 입실은 단순한 부가 정보가 아니었다. 이 기능은 방의 상태 판단뿐 아니라, 이후 실제 운영 기준이 어디서 시작되는지도 연결되는 정보였다.
결국 예정 입실은 “입실자 한 명의 부가 정보”라기보다, 특정 방에 앞으로 어떤 사용 계획이 잡혀 있는지를 보여주는 문제에 더 가까웠다.
7. 예정 입실을 방 기준 예약 상태로 재해석했다
이 지점에서 해석을 조금 바꾸게 되었다.
처음에는 예정 입실을 “앞으로 들어올 사람의 정보”로 생각했다면, 이후에는 이를 “방의 미래 배정 상태”로 보는 것이 더욱 적절했다.
운영자가 실제로 궁금한 것도 마찬가지였다.
- 이 방은 지금 공실인가
- 이 방은 곧 이실이 예정되어 있는가
- 다른 문의를 받아도 되는가
- 현 입실자가 퇴실한 뒤 바로 다음 입실자가 이지는가
- 실제 월세 기준은 언제부터 시작되는가
이 질문들은 결국 사람보다 방의 상태와 방 사용의 시작 시점에 더 가깝다. 그래서 예정 입실기능은 입실자 데이터에 덧붙는 보조 정보로 보기보다, 방에 대한 예약 상태를 관리하는 기능을 보는 것이 더 자연스럽다.
이렇게 생각하니 기능의 방향이 훨씬 또렷해졌다. 예정 입실은 단순히 미래 날짜를 보여주는 것이 아니라 앞으로 방이 어떻게 사용될지를 예약 상태로 표현하는 기능이 되었다.
8. 데이터는 이렇게 나누어 보기로 했다
이 해석을 바탕으로 데이터도 다음과 같이 나누었다.
8.1 방 데이터

- id값은 TEXT 형태로 101 값이 들어간다
- floor는 1층인지 2층인지 들어가게 되는데 101면 → 1, 201면 → 2 이렇게 들어간다.
- name은 101이면 → 1층 01호 이렇게 들어가게 된다.
- room_type은 총무님이 주신 값을 기반으로 들어간다. 이것에 대한 값은 다음 블로그에서 확인할 수 있다.
https://wo-dbs.tistory.com/370
[외주] 미팅 이후, 운영에 맞게 화면을 더 구체화했다 및 좀 더 어려운 요구사항.. - 3/24
저번에 미팅했던 요구사항에 대해 개선 작업을 하였고 크게 다음과 같이 진행하였다.입실자 장부 리스트 보완연간 캘린더 축소 버전 추가도면 상세 정보 강화 및 신규 입실자 등록 흐름 추가 3/1
wo-dbs.tistory.com
- created_at은 값의 생성 날짜라 now 시간 값이 들어간다.
8.2 현재 입실자 데이터 및 예약 데이터
- 이 데이터 컬럼은 밑에 있는 블로그에 설명되어있는 원장님께 받은 자료에서 컬럼을 구성하기도 하였고 필요한 컬럼도 추가하였다.
https://wo-dbs.tistory.com/378
[외주] 고시원의 자료를 받은 미팅 - 3/29
https://wo-dbs.tistory.com/376 [외주] 카톡으로 받은 요구사항 정리해보기(겹치는 요구사항 발생) - 3/28다음 글에서 요구사항 명세서를 전달해드리고 카톡으로 받은 요구사항을 정리해보려고 한다.https:
wo-dbs.tistory.com

contracts이 테이블은 → 계약 테이블 (과거 + 현재 통합) + 이력이 되며 현재 입실중 사람들도 여기서 볼 수 있다.
- rooms와 1:N 관계로 진행된다. 왜냐하면 한 방에 1명만 입실이 가능하지만 여러명의 이력을 남길 수 있기 때문이다.
- name → 이름
- phone → 010-0000-0000 이런 형태로 저장된다. 프론트엔드에서 무조건 이렇게 저장되게 만들었다.
- age → 나이
- purpose와 real_state_agency는 입주 목적과 부동산인데 값은 원장님께 다음과 같이 받았다. 또한, 이 값을 직접 화면에서 값을 추가할 수 있도록 만들었다.
https://wo-dbs.tistory.com/375
[외주] 요구사항 명세서 작성 및 사용자에게 전달 - 3/26
밑 개발 일지 이후로 요구사항 명세서를 작성해야 겠다는 결심이 섰다.. 개발을 하면 할 수록 어려운 기능들이 붙기 시작했기 때문이다.https://wo-dbs.tistory.com/370 [외주] 미팅 이후, 운영에 맞게 화
wo-dbs.tistory.com
- contract_start_date, contract_end_date, contract_months
- contract_start_date는 계약서의 계약일(시작)이다. 시나리오에서 총무님이 계약서를 토대로 넣는 값이다.
- contract_end_date는 계약서의 계약일(끝)이다. 시나리오에서 총무님이 계약서를 토대로 넣는 값이다.
- contract_months는 개월 수인데 입실자가 3개월 살 것이다. 하면 3개월을 넣어주면 되는데 이때 contract_start_date값을 넣고 3개월을 넣으면 자동으로 계산되고 contract_end_date에 값이 들어간다. 이건 나중에 밑 화면 부분에서 좀 더 설명을 하겠다. → 밑 9.1번의 [계약일(끝) 자동 계산]
- monthly_rent는 입실자가 직접 내야하는 월세이다. 이것을 따로 둔 이유는 방에 월세가 적혀있지만 할인이 들어갈 수 있기 때문이다 == 나처럼
- contract_deposit는 계약금(원)이다. 총무님께서 직접 계약서에 있는 보증금을 넣는 자리이다.
- deposit_total는 처음에 contract_deposit값이 들어가면 이 값도 초기화되는데 이건 보증금 관리 이력에서 차감 부분에서 쓰이게 된다. contract_deposit에 값을 넣었을 때 deposit_total에 값이 들어가는 걸 밑 화면에서 좀 더 설명하겠다. → 밑 9.1번의 [금액(관포) && 계약금]
- deposit_returned, deposit_returned_at는 보증금 반환이다. deposit_returned는 처음에 false값이 저장된다.
- 보증금 관리 이력에서 보증금 반환을 하면 deposit_returned는 true로 바뀌고 deposit_returned_at은 보증금 반환 버튼을 누른 날짜 값이 들어간다. → 이건 보증금 관리 이력 블로그에서 해보겠다.
- created_at는 값 생성 날짜이다. → Default로 now가 들어간다.
- updated_at는 컬럼 값이 업데이트 된 날짜이다. → Default로 now가 들어간다.
9. 이 데이터를 실제 화면과 로직에서 어떻게 사용했는가
9.1 현재 입실 중인 사람의 판단

우선, 신규 입실자 등록을 하였을 때 DB에 어떻게 값이 들어가는지 보자
| 화면 필드 | DB 컬럼 | 비고 |
| 호실 선택 | room_id | |
| 이름 | name | |
| 연락처 | phone | |
| 성별 | gender | |
| 나이 | age | |
| 계약일(시작) | contract_start_date | 필수 |
| 개월 수 | contract_months | |
| 계약일(끝) | contract_end_date | 자동 계산값 |
| 금액(관포) | monthly_rent | 입력값 × 10000 (만원→원) |
| 계약금 | contract_deposit | |
| 계약금 잔금 | deposit_total | contract_deposit의 값이 들어감 |
| 거주 목적 | purpose | |
| 부동산 | real_estate_agency | |
| 입실일 | actual_move_in_date | |
| 퇴실일 | actual_move_out_date | |
| (고정) | status | 항상 'scheduled' |
입실을 넣게 되면 위와 같이 들어가게 된다. 이때 중요한 것을 보자
9.1 공실 일때의 신규 입실자 등록 → [계약일(끝) 자동 계산]
- contract_start_date(계약일(시작))에서 contract_months(개월수)만 넣어주면 contract_end_date이 자동으로 계산된다.
[코드]
- contractMoveInDate ← 계약일(시작) 입력값
- contractMonths ← 개월 수 입력값
- contractEndDate ← 계약일(끝) — 자동 계산됨
- actualMoveInDate ← 입실일 (다를 때만 입력)
- moveOutDate ← 퇴실일 — 자동 계산됨
사용자가 값을 입력할 때마다 연쇄 계산
- 계약일(시작) 입력 → contractMoveInDate 저장, 만약 개월 수가 이미 있으면: contractEndDate 자동 계산
- 계약일(시작) 먼저 → 개월 수 나중 입력: 개월 수 입력 시점에 계산됨
→ 즉, 2개의 값이 있어야 계산이 됨
function handleContractStartChange(val: string) {
setContractMoveInDate(val);
const months = Number(contractMonths);
if (val && months > 0) setContractEndDate(calcEndDate(val, months));
}
2. 개월 수 입력
- contractMonths 저장
- 계약일(시작)일이 있으면: contractEndDate 자동 계산
- (입실일 or 시작일)이 있으면: moveOutDate도 자동 계산 -> 이건 예정 입실이라 나중에
→ 개월 수를 먼저 입력해도 contractEndDate 계산은 안 되고, 이후에 계약일(시작)을 입력하는 순간 handleContractStartChange에서 그때 계산됨
→ 개월 수 먼저 → 계약일 나중 입력: 계약일 입력 시점에 계산됨
function handleContractMonthsChange(val: string) {
setContractMonths(val);
const months = Number(val);
if (contractMoveInDate && months > 0) setContractEndDate(calcEndDate(contractMoveInDate, months));
const base = actualMoveInDate || contractMoveInDate;
if (base && months > 0) setMoveOutDate(calcEndDate(base, months));
}
3. 계약일(끝) 자동 계산 함수
- 로직: 시작일 + N개월 - 1일 (월말 자동 처리) → 2026-01-01 + 3개월 - 1일 = 2026-03-31 단, 2월은 + 28일
- 2026-01-15, 3개월 1월(30) + 2월(28) + 3월(30) = 88일 → 2026-01-15 + 88 - 1 = 2026-04-12
→ 이건 공용으로 사용
/** 입실일 + N개월 - 1일 (2월=28일, 나머지=30일 고정) */
export function calcEndDate(startDate: string, months: number): string {
const start = new Date(startDate);
let totalDays = 0;
for (let i = 0; i < months; i++) {
const d = new Date(start);
d.setMonth(d.getMonth() + i);
totalDays += d.getMonth() === 1 ? 28 : 30;
}
const result = new Date(start);
result.setDate(result.getDate() + totalDays - 1);
return result.toISOString().slice(0, 10);
}
[Supabase 등록 코드]
export async function insertContract(input: NewContractInput): Promise<DbContract> {
const supabase = createClient();
const { data, error } = await supabase
.from('contracts')
.insert(input)
.select()
.single();
if (error) throw error;
return data as DbContract;
}
9.1 공실 일때의 신규 입실자 등록 → [예정 입실 정보를 비워두기]
- 예정 입실 정보를 비워둔다는 것은 총무님이 실제 계약서에 있는 것만 넣는다라는 것 즉, 입실자가 그냥 계약일에 맞추어서 들어오거나 아직 입실일을 변경하지 않은 것
- 즉 위 화면에서 입실 정보에 대한 데이터가 없을 때이다.
그래서 컬럼은 다음과 같다.
- contract_start_date(계약일(시작)), contract_months(개월수), contract_end_date가 아닌 actual_move_in_date(실제 입실일) = null, actual_move_out_date(실제 퇴실일) = null 로 저장된다.
- useEffectiveRooms에서 입실일 계산 시
const moveInStr = (c.actual_move_in_date ?? c.contract_start_date).slice(0, 10);
→ actual_move_in_date가 null이면 contract_start_date를 입실일로 대신 사용 퇴실일도 동일하게 actual_move_out_date ?? contract_end_date 순으로 적용된다.
9.1 공실 일때의 신규 입실자 등록 → [금액(관포) && 계약금]
금액(관포) → monthly_rent 컬럼 (저장 시 입력값 × 10,000, 만원→원)
[계약금 입력 시 흐름]
- 신규 등록(NewResidentModal)과 예정 입실 추가(TenantListTable) 둘 다
- 계약금 입력값
- contract_deposit: 입력값
- deposit_total:입력값 (동일하게 저장)
두 컬럼의 역할 차이
| 컬럼 | 역할 |
| contract_deposit | 최초 계약 시 낸 계약금 |
| deposit_total | 보증금 관리 이력에서 쓰는 총 보증금 (차감 기준) |
deposit_total은 나중에 보증금 관리 이력에서 직접 수정 가능 (총 보증금 클릭 → 인라인 편집)
9.2 현재 입실 중인 방의 판단
| 필드 | DB 컬럼 | 비고 |
| 계약일(시작) | contract_start_date | 필수 |
| 계약일(끝) | contract_end_date | 자동 계산값 |
| 입실일 | actual_move_in_date | |
| 퇴실일 | actual_move_out_date | |
| (고정) | status | 항상 'scheduled' |
- useEffectiveRooms.ts에서 날짜 문자열 비교로 판단한다.
// scheduled 계약들 중에서 "현재 입실 중"인 사람 찾기
const current = scheduledContracts
.filter((c) => {
const moveInStr = (c.actual_move_in_date ?? c.contract_start_date).slice(0, 10);
if (moveInStr > todayStr) return false; // 입실일이 오늘 이후 → 제외
const moveOutDate = c.actual_move_out_date ?? c.contract_end_date;
if (moveOutDate && moveOutDate.slice(0, 10) <= todayStr) return false; // 퇴실일이 오늘 이전 → 제외
return true; // 입실일 ≤ 오늘 < 퇴실일 → 현재 입실 중
})
조건 정리
- scheduled 계약들 중에서 "현재 입실 중"인 사람 찾기부터 진행
- 입실일 ≤ 오늘 AND (퇴실일 없음 OR 퇴실일 > 오늘) → 입실일 ≤ 오늘 = 입실일이 오늘이거나 이미 지났다 → 이미 들어온 상태
- 입실일 → actual_move_in_date(실제 입실일) (없으면 contract_start_date)
- 퇴실일 → actual_move_out_date(실제 퇴실일) (없으면 contract_end_date)
ex ) 오늘: 2026-04-19
- 입실일 2026-04-01 ≤ 2026-04-19 → 이미 들어왔음
- 퇴실일 2026-07-01 > 2026-04-19 → 아직 안 나감
→ 입실 중 반대로 입실일 > 오늘이면 아직 안 들어온 것 → 예정 입실(입실 전)로 제외된다.
9.3 예정 입실은 어떤 상태로 해석했는가 → 중요
여기서는 현재 입실자와 예약 기능을 둘러봐야한다. 총 3가지를 보려고 한다.
- 방에 입실자가 있을 때 예정 입실 데이터 등록
- 방에 원래 살고 있는 입실자 퇴실 후 등록되어있는 예정 입실자 입실
- 공실일 때의 예정 입실
이번에는 화면이 다 다르기 때문에 각 포인트 마다 화면을 보려고 한다.
9.3 예정 입실은 어떤 상태로 해석했는가 → [입실자가 있을 때 예정 입실 데이터 등록 && 예정 입실자 판단]
현재 102호에 다음과 같이 고시원이라는 입실자가 살고 있다.
- 계약일은 26년 1월 19일 ~ 27년 1월 18일이다.


[위 상태에서 예정 입실자 등록]



- 위와 같이 Data를 입력했을 때 다음과 같이 들어감.
- 아까 위에서 9.1 공실 일때의 신규 입실자 등록 → [예정 입실 정보를 비워두기]에서 한 것이랑 똑같다.
DB 저장 값 (handleAddScheduled → insertContract)
| 화면 필드 | DB 컬럼 | 비고 |
| 호실 선택 | room_id | |
| 이름 | name | |
| 연락처 | phone | |
| 성별 | gender | |
| 나이 | age | |
| 계약일(시작) | contract_start_date | 필수 |
| 개월 수 | contract_months | |
| 계약일(끝) | contract_end_date | 자동 계산값 |
| 금액(관포) | monthly_rent | 입력값 × 10000 (만원→원) |
| 계약금 | contract_deposit | |
| 계약금 잔금 | deposit_total | contract_deposit의 값이 들어감 |
| 거주 목적 | purpose | |
| 부동산 | real_estate_agency | |
| 입실일 | actual_move_in_date | |
| 퇴실일 | actual_move_out_date | |
| (고정) | status | 항상 'scheduled' |
[코드]
async function handleAddScheduled(roomId: string, record: ScheduledResident) {
await addContract({
room_id: roomId,
name: record.name,
phone: record.phone,
gender: record.gender ?? null,
age: record.age ?? null,
purpose: record.purpose ?? null,
real_estate_agency: record.realEstateAgency ?? null,
contract_start_date: record.contractMoveInDate,
contract_end_date: record.contractEndDate ?? null,
contract_months: record.contractMonths ?? null,
actual_move_in_date: record.actualMoveInDate ?? null,
actual_move_out_date: record.moveOutDate ?? null,
monthly_rent: record.monthlyRent ?? null,
contract_deposit: record.contractDeposit ?? null,
deposit_total: record.contractDeposit ?? null,
status: 'scheduled',
});
}
export async function insertContract(input: NewContractInput): Promise<DbContract> {
const supabase = createClient();
const { data, error } = await supabase
.from('contracts')
.insert(input)
.select()
.single();
if (error) throw error;
return data as DbContract;
}
[예정 입실자 판단]
판단 로직 (useEffectiveRooms, TenantListTable)
- useEffectiveRooms는 현재 입실자만 찾고, 예정 입실자는 TenantListTable의 scheduledData에서 따로 필터링한다.
- 현재 입실자 판단 → useEffectiveRooms
export function useEffectiveRooms() {
const { rooms, contracts, loading, error, refetch } = useRooms();
const { today, todayStr } = useToday();
// 오늘 날짜 기준으로 방 상태 계산 (YYYY-MM-DD 문자열 비교로 타임존 버그 방지)
const effectiveRooms = useMemo(() => {
return rooms.map((room) => {
// ── Case 1: active 계약이 있는 방 — 퇴실일이 안 지났으면 그대로 ──
if (room.status === 'occupied') {
const moveOut = room.moveOutDate;
if (!moveOut || moveOut.slice(0, 10) > todayStr) {
return room; // 아직 거주 중
}
// 퇴실일 경과 → 아래 scheduled 검색으로 fall-through
} else if (room.status !== 'contract') {
return room; // vacant 등 변경 불필요
}
// ── Case 2 (+ Case 1 퇴실 후): scheduled 계약에서 현재 입실자 계산 ──
// 조건: 입실일 ≤ 오늘 AND (퇴실일 없음 OR 퇴실일 > 오늘)
// → 같은 방에 이전 입실자·신규 입실자가 모두 scheduled여도 정확히 구분
const scheduledContracts = contracts.filter(
(c) => c.room_id === room.id && c.status === 'scheduled'
);
const current = scheduledContracts
.filter((c) => {
const moveInStr = (c.actual_move_in_date ?? c.contract_start_date).slice(0, 10);
if (moveInStr > todayStr) return false; // 아직 입실 전
const moveOutDate = c.actual_move_out_date ?? c.contract_end_date;
if (moveOutDate && moveOutDate.slice(0, 10) <= todayStr) return false; // 이미 퇴실
return true;
})
.sort((a, b) => {
// 가장 최근에 입실한 사람 우선
const aDate = a.actual_move_in_date ?? a.contract_start_date;
const bDate = b.actual_move_in_date ?? b.contract_start_date;
return bDate.localeCompare(aDate);
})[0];
- 예정 입실자 판단 → TenantListTable.tsx
const pendingContracts = contracts.filter((c) => {
if (c.status !== 'scheduled') return false;
const moveInStr = (c.actual_move_in_date ?? c.contract_start_date).slice(0, 10);
return moveInStr > todayStr; // moveIn > 오늘 → 예정 입실
});
같은 방에 여러 scheduled 계약이 있을 때 날짜로 구분
- 현재 입실자: moveIn ≤ 오늘 AND (moveOut 없음 OR moveOut > 오늘)
- 예정 입실자: moveIn > 오늘
예) 오늘 2026-04-19
- 고시원: contract_start_date=2026-01-01, contract_end_date=2026-07-31 → moveIn(01-01) ≤ 오늘 AND moveOut(07-31) > 오늘 → 입실 중
- 정재윤: contract_start_date=2026-08-01, contract_end_date=2026-10-31 → moveIn(08-01) > 오늘 → 예정 입실 (예약 목록에만 표시)
화면 표시 → useEffectiveRooms
| 구분 | 표시 위치 |
| 현재 입실자 | 입실자 목록 테이블 이름/상태 |
| 예정 입실자 | 예약(예정 입실) 버튼 → 모달 |
현재 입실자와 예정 입실자가 같은 방에 공존하는데, 날짜로 구분된다.
→ 입실자 목록 테이블에서는 현재 입실자만 이름/상태에 표시
→ 예약(예정 입실) 버튼 클릭 시 모달에서 예정 입실자 확인 가능
날짜가 바뀌어 예정 입실자의 입실일이 되면
- 현재 입실자의 퇴실일 ≤ 오늘 → 현재 입실자 제외
- 예정 입실자의 입실일 ≤ 오늘 → 새 입실자로 자동 전환
9.3 예정 입실은 어떤 상태로 해석했는가 → [입실자 퇴실 및 예정 입실자 입실]
입실자가 퇴실 날짜가 되었을 때 어떻게 판단해하고 자동으로 예정 입실자가 현재 입실자가 되는지 보자
[입실자 퇴실 판단]
퇴실 판단 — useEffectiveRooms 필터 조건 → 위에 있는 코드
const moveOutDate = c.actual_move_out_date ?? c.contract_end_date;
if (moveOutDate && moveOutDate.slice(0, 10) <= todayStr) return false; // 퇴실
퇴실일 ≤ 오늘 이 되는 순간 현재 입실자 목록에서 자동 제외된다.
[예정 입실자의 입실]
예정 입실자 → 현재 입실자 전환 별도 처리 없이 같은 필터가 처리한다.
오늘: 2026-08-01
- 고시원: moveOut = 2026-07-31 → 2026-07-31 ≤ 2026-08-01 → 제외
- 정재윤: moveIn = 2026-08-01 → 2026-08-01 ≤ 2026-08-01 (오늘) → 현재 입실자로 자동 전환
DB 컬럼상으로는 다음과 같다.
- 강지수 제외 판단 → actual_move_out_date (없으면 contract_end_date) ≤ todayStr
- 정재윤 입실 판단 → actual_move_in_date (없으면 contract_start_date) ≤ todayStr
코드상으로는 useEffectiveRooms.ts
const moveInStr = (c.actual_move_in_date ?? c.contract_start_date).slice(0, 10);
if (moveInStr > todayStr) return false;
const moveOutDate = c.actual_move_out_date ?? c.contract_end_date;
if (moveOutDate && moveOutDate.slice(0, 10) <= todayStr) return false;
전체 흐름 요약
- 매 렌더링마다 todayStr 기준으로 재계산
- 퇴실일 ≤ 오늘 → 강지수 제외 && 입실일 ≤ 오늘 → 정재윤 current로 선택
- room.status = 'occupied', room.resident = '정재윤', room.contractId = 정재윤 계약 ID
헤더 날짜가 바뀌거나 페이지를 새로고침하면 자동으로 반영됩니다. 별도 버튼이나 DB 업데이트 없이 날짜만으로 전환된다.
9.3 예정 입실은 어떤 상태로 해석했는가 → [공실일 때의 예정 입실]
buildRooms → useEffectiveRooms 2단계로 계산된다.
1단계: buildRooms → DB의 scheduled로 판단
- scheduled 계약 있음 → status = 'contract'
- 없음 → status = 'vacant'
2단계: useEffectiveRooms
useEffectiveRooms.ts:
} else if (room.status !== 'contract') {
return room; // vacant → 그대로 반환
}
contract 상태만 아래 로직으로 진입
- scheduled 계약 중 moveIn ≤ 오늘인 게 있음?
- 있음 → occupied (입실중) 으로 승격
- 없음 → vacant 로 변경 (전부 미래 예약)
예시
- 오늘: 2026-04-19, 공실 방에 정재윤 예약 (입실일: 2026-08-01)
- buildRooms: status = 'contract'
- useEffectiveRooms: moveIn(08-01) > 오늘(04-19) → current 없음 → vacant 반환
- 화면: 공실 + 예약(예정 입실) 1건 버튼 표시
- 오늘: 2026-08-01 (입실날)
- useEffectiveRooms: moveIn(08-01) ≤ 오늘(08-01) → current = 정재윤 → occupied 반환
- 화면: 입실중, 이름: 정재윤
10. 조회가 많아질 것을 고려해 인덱스도 함께 고민했다
예정 입실 기능을 정리하면서 느낀 점은, 데이터를 어떻게 저장할지만큼이나 어떤 기준으로 자주 조회되는가도 중요하다는 점이었다. 특히, 이 기능은 단순히 계약 한 건을 저장하는 데서 끝나지 않고, 다음과 같은 조회가 반복해서 일어난다.
- 현재 입실 중인 사람 찾기
- 특정 방에 연결된 예약(예정 입실) 목록 찾기
- 예정 입실 여부를 방 단위로 판단하기
- 날짜 기준으로 현재 입실 / 예정 입실을 나누기
즉 이 기능은 입력보다 조회가 더 자주 일어나는 구조에 가까웠다. 그래서 나도 컬럼을 나누는 것에서 멈추지 않고, 실제로 많이 쓰일 조회 조건을 기준으로 인덱스를 어떻게 가져갈지를 같이 고민해보게 되었다.
10.1 status 단독 인덱스
CREATE INDEX idx_contracts_status
ON contracts(status);
가장 먼저 떠올린 것은 status 단독 인덱스였다. 현재 계약 테이블에서는 status = 'scheduled' 조건으로 데이터를 자주 걸러내고 있다. 실제로 현재 입실자 판단도, 예정 입실 판단도 먼저 scheduled 계약을 기준으로 필터링한 뒤 날짜 조건을 추가로 보는 식으로 동작한다.
그래서 가장 기본적인 출발점으로는 status 컬럼 단독 인덱스를 두는 것이 자연스럽다고 보았다. 다만 이 인덱스만으로는 방별 조회까지 커버하기 어렵기 때문에, 실제 사용 흐름을 생각하면 이것만으로는 부족했다.
→ room_id + status 복합 인덱스
CREATE INDEX idx_contracts_room_status
ON contracts(room_id, status);
그다음으로 더 중요하다고 본 것은 room_id + status 복합 인덱스였다. 이유는 예정 입실 기능이 결국 방 기준으로 해석되는 기능이었기 때문이다.
실제 화면에서는 특정 방에 대해
- 현재 연결된 계약이 무엇인지
- scheduled 상태의 계약이 몇 건 있는지
- 그중 현재 입실자인지 예정 입실자인지
를 자주 확인하게 된다. 즉 계약 테이블을 전체로 훑기보다, 방 하나를 기준으로 관련 계약을 좁혀서 보는 경우가 많다는 뜻이다.
이 점을 생각하면 status 단독 인덱스보다 room_id, status 순서의 복합 인덱스가 더 실용적일 수 있다고 판단했다. 특히 같은 방에 여러 계약 이력이 쌓일 수 있는 구조에서는 방 단위로 먼저 좁히는 것이 조회 흐름과 더 잘 맞는다.
10.2 날짜 조건까지 함께 보는 인덱스
CREATE INDEX idx_contracts_room_status_dates
ON contracts(room_id, status, contract_start_date, actual_move_in_date);
마지막으로 고민한 것은 날짜 조건까지 포함한 인덱스였다. 현재 로직에서는 단순히 scheduled 계약을 찾는 것에서 끝나지 않고, 그 안에서 다시
- 실제 입실일이 오늘보다 이전인지
- 아직 퇴실하지 않았는지
- 예정 입실인지 현재 입실인지
를 날짜 비교로 판별하고 있다. 그래서 장기적으로는 room_id, status뿐 아니라 날짜 컬럼까지 함께 고려한 인덱스가 도움이 될 수 있다고 보았다.
예를 들어 room_id, status, contract_start_date, actual_move_in_date처럼 구성하면 방별 scheduled 계약을 찾은 뒤 날짜 비교로 이어지는 흐름과 맞닿는 부분이 있다. 다만 여기서는 한 가지 애매한 점도 있었다.
내 로직은 다음과 같다.
- 실제 입실일(actual_move_in_date)이 있으면 그걸 입실일로 씀
- 없으면 계약 시작일(contract_start_date)을 입실일로 대신 씀
즉 코드는 “왼쪽 값이 있으면 왼쪽, 없으면 오른쪽” 이라는 뜻
- 입실일: actual_move_in_date ?? contract_start_date
- 퇴실일: actual_move_out_date ?? contract_end_date
예를 들어 DB에 이런 데이터가 있다고 해보자
| name | contract_start_date | actual_move_in_date |
| A | 2026-03-24 | null |
| B | 2026-03-24 | 2026-03-28 |
오늘이 2026-03-26이라고 해보자.
A의 입실일 판단
- actual_move_in_date가 없음
- 그래서 contract_start_date 사용
- 입실일 = 2026-03-24
B의 입실일 판단
- actual_move_in_date가 있음
- 그래서 그 값 사용
- 입실일 = 2026-03-28
즉 A와 B는 입실일을 보는 기준 컬럼이 다름 그런데 SQL을 다음과 같이 쓰면
WHERE contract_start_date <= '2026-03-26'
A도 잡히고 B도 잡혀 → 왜냐하면 둘 다 contract_start_date = 2026-03-24 그런데 실제 의미상으로는 B는 아직 입실 전임 → 왜? 실제 입실일이 2026-03-28이니까.
그래서 필요한 게 COALESCE(...) → SQL에서는 JS의 ?? 같은 걸 보통 COALESCE()로 표현한다.
COALESCE(actual_move_in_date, contract_start_date)
의미:
- actual_move_in_date가 있으면 그걸 쓰고
- 없으면 contract_start_date를 써라
그래서 현재 입실 여부를 판단하려면 다음과 같이 하지말고
WHERE contract_start_date <= today
다음과 같이 하면 됨 == 무조건 계약 시작일만 입실일로 보겠다
WHERE COALESCE(actual_move_in_date, contract_start_date) <= today
즉 날짜 판단 기준이 한 컬럼으로 고정되어 있지 않고, 값이 없으면 다른 컬럼을 대신 쓰는 구조다.
이 때문에 단순히 WHERE contract_start_date <= today 같은 형태로 예쁘게 떨어지지 않고, 실제로는 COALESCE(actual_move_in_date, contract_start_date) 같은 조건이 필요해질 수 있다.
이런 방식은 DB에서 필터링 자체는 가능하지만, 일반적인 단일 컬럼 비교처럼 인덱스를 깔끔하게 타지 못할 가능성이 있다. 그래서 날짜 인덱스는 무조건 추가한다기보다, 실제 조회를 어느 정도 DB 쪽으로 내릴 것인지까지 함께 보면서 판단해야 하는 부분이라고 생각했다.
이 부분에 대해서는 실제 테스트를 해보고 성능 개선이 좋다고 생각한다. → COALESCE
10.3 느낀 점
이 부분을 정리하면서 느낀 것은, 인덱스 역시 단순히 “많이 쓸 것 같은 컬럼”에 다는 것이 아니라는 점이었다.
결국 중요한 것은 어떤 화면에서 어떤 조건으로 데이터를 읽을 것인가였다.
예정 입실 기능도 처음에는 단순히 컬럼 몇 개를 추가하는 문제처럼 보였지만, 실제로는
- 방 기준 조회가 많은지
- scheduled 상태를 자주 거르는지
- 날짜 기준 비교가 어디까지 DB로 내려갈지
- 현재 입실자와 예정 입실자를 어떤 방식으로 구분할지
같은 흐름까지 함께 봐야 했다. 그래서 이번 기능에서는 데이터 모델을 나누는 것과 조회 흐름을 정리하는 것, 그리고 그에 맞는 인덱스를 고민하는 것이 하나로 연결되어 있다는 점을 더 강하게 느끼게 되었다.
11. 마무리
이번 예정 입실 기능을 정리하면서 가장 크게 느낀 점은, 요구사항은 문장 그대로 구현되는 것이 아니라는 점이었다.
처음에는 “예정 입실을 보여주면 된다”는 문장으로 시작했지만, 실제로 구현을 생각해보니 그 안에는 계약일과 입실일의 구분, 현재 상태와 미래 상태의 분리, 목록과 상세의 조회 차이, 방 기준 관리 같은 여러 문제가 함께 들어 있었다.
특히 고시원에서는 계약일과 실제 입실일이 다를 수 있고, 월세 납부 기준 역시 실제 입실일을 따라간다. 이 점까지 고려해보면 예정 입실은 단순한 미래 날짜 관리가 아니라, 실제 운영 기준이 언제부터 시작되는가를 함께 다루는 기능에 더 가까웠다.
그래서 이 기능을 정리하는 과정에서 예정 입실을 입실자 한 명의 부가 정보로 보기보다, 방의 미래 배정 상태를 관리하는 예약 데이터로 보는 쪽이 더 자연스럽다고 판단하게 되었다. 그렇게 해석이 바뀌고 나니 데이터 구조도, 화면도, 조회 방식도 조금씩 더 명확하게 정리되기 시작했다.
이번 글에서는 예정 입실 기능을 중심으로, 요구사항을 실제 데이터 모델과 조회 로직으로 어떻게 풀어갈 수 있을지를 정리해보았다.
'외주 > 고시원 외주 개발 일지' 카테고리의 다른 글
| [외주] 기획 요구사항에서 데이터 모델과 조회 로직까지 정리한 과정 - 공과금(한전) 공실 기간 정산을 시스템에서 어떻게 표현할지 (기능 업데이트) (2) | 2026.04.24 |
|---|---|
| [외주] 기획 요구사항에서 데이터 모델과 조회 로직까지 정리한 과정 - 보증금 차감과 반환을 하나의 흐름으로 볼지 분리할지 (1) | 2026.04.24 |
| [외주] 배포하기 및 로그인 및 Supabase - 4/18 (1) | 2026.04.19 |
| [외주] 현금 승계 PDF로 만들기 - 4/8 (0) | 2026.04.14 |
| [외주] 클라이언트 앞에서 PT를 진행하다! - 4/8 (0) | 2026.04.13 |