inflearn logo
강의

Course

Instructor

[Daily Completion Challenge] <From Planning to Launch with FastAPI> with the Author

Sections 8.3 ~ 8.4: Implementing Timeslot Management and Reservation APIs (p299~p321)

4주 2회차 과제

Resolved

49

Green Lee

2 asked

0

안녕하세요.

 

과제를 잘 못 이해한건가 싶기도 하지만 아래와 같은 조건하에 구현을 해 봤습니다.

 

수업 과정에서 Front-end는 고정인 상황이기 때문에 기존 API는 변경이 없다는 가정하에 변경 해 봤습니다.

  1. 시간대1개와 요일n개의 설정이 하나의 row로 묶여있는 상태로는 요일별 시간대 변동에 대응하기 까다로운 것 같아 요일별 개별 row로 저장하도록 일부 수정 하였습니다. API endpoint 모델 변경을 피하기 위해서 내부적으로 여러개의 요일별 row로 나누어 저장하도록 했습니다.

     

  2. 고민중에 작가님께서 올려 놓으신 레퍼런스 코드를 봤는데, 책에서 언급하신 것 처럼 postgres 경우와 sqlite 경우로 코드가 분기 되는 것을 보았고, 지금은 교육 과정이기 때문이라고 생각 하지만 배포 코드와 개발 환경 코드가 다른것은 여러모로 좋지 않은 것 같아 현재는 sqlite 기준으로 개별 DB구현에서만 지원하는 것은 배제하는 방향으로 구현 했습니다.

     

  3. 1과 같이 변경 함으로써, 기 등록된 정보의 수정에서는 개별 날짜 별 시간대 설정 면에서 자유도가 생겼다고 생각 됩니다.

  4. 타임슬롯의 변경과 삭제에 관해서는 내부적으로는 time-slot으로 관리하지만, 외부공개 id는 아니므로, 키 로서 start-time, end-time, weekday (API형태로서는 리스트)조합으로 정의 해서 동작 하도록 구현했습니다.

  5. 특히 삭제의 경우는 부득이 delete method의 경우는 payload가 포함되는것이 받아들여지지 않는 경우도 있는것을 고려해서, 새 등록 값 두가지(new_start_time과, new_end_time) 값이 모두 제공되고 같은 경우를 특정하여 삭제 동작으로 정의하여 동작하도록 구현했습니다.

 

class TimeSlotUpdateByGroupIn(SQLModel):
    start_time: time
    end_time: time
    weekdays: Weekdays
    new_start_time: time | None = None
    new_end_time: time | None = None
    new_weekdays: Weekdays | None = None

    @model_validator(mode="after")
    def check_update_fields(self):
        update_fields = {
            "new_start_time": self.new_start_time,
            "new_end_time": self.new_end_time,
            "new_weekdays": self.new_weekdays,
        }
        if not any(value is not None for value in update_fields.values()):
            raise ValueError("최소 하나의 수정 필드는 반드시 제공되어야 합니다.")
        return self

 

@router.post("/time-slots", status_code=status.HTTP_201_CREATED, response_model=TimeSlotOut)
async def create_time_slot(
    user: CurrentUserDep,
    session: DbSessionDep,
    payload: TimeSlotCreateIn,
) -> TimeSlotOut:
    if not user.is_host:
        raise GuestPermissionError()

    weekdays = sorted(set(payload.weekdays))

    # dup. check with already exist one
    stmt = select(TimeSlot).where(
        and_(
            TimeSlot.calendar_id == user.calendar.id,
            TimeSlot.weekday.in_(weekdays),
            TimeSlot.start_time < payload.end_time,
            TimeSlot.end_time > payload.start_time,
        )
    )
    result = await session.execute(stmt)
    existing_time_slot = result.scalars().first()
    if existing_time_slot:
        raise TimeSlotOverlapError()

    time_slots = [
        TimeSlot(
            calendar_id=user.calendar.id,
            start_time=payload.start_time,
            end_time=payload.end_time,
            weekday=weekday,
        )
        for weekday in weekdays
    ]

    session.add_all(time_slots)
    await session.commit()

    if not time_slots:
        raise TimeSlotOverlapError()

    return TimeSlotOut(
        start_time=time_slots[0].start_time,
        end_time=time_slots[0].end_time,
        weekdays=weekdays,
        created_at=time_slots[0].created_at,
        updated_at=time_slots[0].updated_at,
    )


@router.get(
    "/time-slots/{host_username}",
    status_code=status.HTTP_200_OK,
    response_model=list[TimeSlotOut],
)
async def get_host_timeslots(
    host_username: str,
    session: DbSessionDep,
) -> list[TimeSlotOut]:
    stmt = (
        select(User)
        .where(User.username == host_username)
        .where(User.is_host.is_(true()))
    )
    result = await session.execute(stmt)
    host = result.scalar_one_or_none()
    if host is None or host.calendar is None:
        raise HostNotFoundError()
    
    stmt = select(TimeSlot).where(TimeSlot.calendar_id == host.calendar.id)
    result = await session.execute(stmt)
    time_slots = result.scalars().all()

    grouped: dict[tuple[time, time], TimeSlotOut] = {}
    for time_slot in time_slots:
        key = (time_slot.start_time, time_slot.end_time)
        if key not in grouped:
            grouped[key] = TimeSlotOut(
                start_time=time_slot.start_time,
                end_time=time_slot.end_time,
                weekdays=[time_slot.weekday],
                created_at=time_slot.created_at,
                updated_at=time_slot.updated_at,
            )
            continue

        grouped[key].weekdays.append(time_slot.weekday)
        if time_slot.created_at < grouped[key].created_at:
            grouped[key].created_at = time_slot.created_at
        if time_slot.updated_at > grouped[key].updated_at:
            grouped[key].updated_at = time_slot.updated_at

    if not grouped:
        raise TimeSlotNotFoundError()
    
    for time_slot in grouped.values():
        time_slot.weekdays = sorted(set(time_slot.weekdays))

    return list(grouped.values())

@router.patch(
    "/time-slots",
    status_code=status.HTTP_200_OK,
    response_model=TimeSlotOut | None,
)
async def update_time_slot(
    user: CurrentUserDep,
    session: DbSessionDep,
    payload: TimeSlotUpdateByGroupIn,
) -> TimeSlotOut | None:
    if not user.is_host:
        raise GuestPermissionError()

    current_weekdays = sorted(set(payload.weekdays))
    stmt = select(TimeSlot).where(
        and_(
            TimeSlot.calendar_id == user.calendar.id,
            TimeSlot.start_time == payload.start_time,
            TimeSlot.end_time == payload.end_time,
        )
    )
    result = await session.execute(stmt)
    current_slots = result.scalars().all()
    if not current_slots:
        raise TimeSlotNotFoundError()

    existing_weekdays = sorted({slot.weekday for slot in current_slots})
    if existing_weekdays != current_weekdays:
        raise TimeSlotNotFoundError()

    delete_only = (
        payload.new_start_time is not None
        and payload.new_end_time is not None
        and payload.new_start_time == payload.new_end_time
    )

    if delete_only:
        current_ids = [slot.id for slot in current_slots]
        await session.execute(delete(TimeSlot).where(TimeSlot.id.in_(current_ids)))
        await session.commit()
        return None

    new_start_time = payload.new_start_time or payload.start_time
    new_end_time = payload.new_end_time or payload.end_time
    new_weekdays = sorted(set(payload.new_weekdays or payload.weekdays))

    if new_start_time >= new_end_time:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
            detail="시작 시간은 종료 시간보다 빨라야 합니다.",
        )

    current_ids = [slot.id for slot in current_slots]
    stmt = select(TimeSlot).where(
        and_(
            TimeSlot.calendar_id == user.calendar.id,
            TimeSlot.weekday.in_(new_weekdays),
            TimeSlot.start_time < new_end_time,
            TimeSlot.end_time > new_start_time,
            TimeSlot.id.not_in(current_ids),
        )
    )
    result = await session.execute(stmt)
    existing_time_slot = result.scalars().first()
    if existing_time_slot:
        raise TimeSlotOverlapError()

    await session.execute(delete(TimeSlot).where(TimeSlot.id.in_(current_ids)))
    new_time_slots = [
        TimeSlot(
            calendar_id=user.calendar.id,
            start_time=new_start_time,
            end_time=new_end_time,
            weekday=weekday,
        )
        for weekday in new_weekdays
    ]
    session.add_all(new_time_slots)
    await session.commit()

    return TimeSlotOut(
        start_time=new_start_time,
        end_time=new_end_time,
        weekdays=new_weekdays,
        created_at=new_time_slots[0].created_at,
        updated_at=new_time_slots[0].updated_at,
    )

python aws tdd FastAPI 북-챌린지

Answer 1

0

hannal

과제 의도는 SQL 조건문을 연습하는 것인데, 색다른 접근법을 생각하셨군요! 시간 범위에 요일들이 들어가는 의존 방향을 요일에 타임슬롯이 들어가는 방식이 흥미로워요. 🙂 월요일엔 타임슬롯 1, 2, 3, 화요일엔 타임슬롯 1, 4, 5라고 인식하게 되어 생각흐름에 자연스레 맞춰져 직관적이에요.

수고하셨습니다!

1

Green Lee

피드백 감사합니다.

 

아무래도 제가 너무 요일별 시간관리에 집착했나봅니다 ㅎㅎ

4주 1회차 과제

0

44

2

4주 5회차 과제

0

44

1

4주 5회차 과제 제출

0

52

2

4주 4회차 과제 제출

0

62

2

351쪽 질문

0

52

2

4주 3회차 과제

0

49

2

refresh() 메서드와 픽스처에 대해 질문이 있습니다.

0

59

2

4주 2회차 과제 질문

0

57

3

4주 1회차 과제

0

53

2

4주 1회차 과제

0

44

2

4주 3회차 과제

0

55

2

4주 1회차 과제

0

52

2

4주 3회차 과제

0

38

1

4주 5회차 과제

0

38

2

4주 1회차 과제

0

30

2

4주 1회차 과제

0

25

2

4주 4회차 과제 제출

0

36

2

4주 1회차 과제 제출합니다.

0

43

1

patch 요청시 payload가 넘어가지 않습니다.

0

60

3

4주 1회차 과제

3

119

2

페이지 144 코드 문의

0

61

3

책과 github 코드가 다릅니다 p130

0

50

2

120페이지 코드 질문드립니다.

0

48

2

테스팅과 학습법의 관계 (?)

0

67

2