묻고 답해요
167만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 3회차 과제
PostgreSQL의 Partial Unique Index의 기능을 활용하면 attendance_status 모델필드의 값이 cancelled 된 경우를 제외한 모든 동일 일자와 동일 타임슬롯인 경우를 중복으로 간주하는 제약을 구현 할 수 있습니다. __table_args__ = ( Index( "uq_active_booking_when_timeslot", "when", "time_slot_id", unique=True, postgresql_where=text("attendance_status <> 'CANCELLED'"), ), ) postgresql 일 경우, attendance_status 가 'CANCELLED' 가 아닌 모든 when + time_slot_id 조합에 고윳값 제약을 걸어줍니다.
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
refresh() 메서드와 픽스처에 대해 질문이 있습니다.
픽스처 함수는 픽스처로, 픽스처 내부에서 사용되는 픽스처 함수는 객체로 표현하였습니다.=====host_user_calendar 픽스처에서 refresh() 메서드를 통해 host_user 객체를 db와 동기화해서 host_user 객체가 calendar 객체의 존재를 아는 것으로 이해했는데요.그런데 해당 host_user 객체는 반환되지 않고 host_user_calendar 픽스처에 남아있게 되어 접근할 수 없으니 무의미한 행동이라고 보여졌습니다. 그런데 refresh()를 사용하지 않으면 test_사용자가_변경하는_항목만_변경되고_나머지는_기존_값을_유지한다() 테스트 함수에서 update_calendar 엔드포인트 호출 시 404 에러가 발생하는 것을 확인했습니다. 즉, user.calendar로 접근 시 None으로 평가되는 것이지요. 그렇다면 host_user를 반환하지 않아도 해당 host_user 객체는 client_user_auth 픽스처에서 공유되는 것일까요? (혹은 동일한 객체일까요?) 그래야 refresh()를 진행했을 때 오류가 뜨지 않는다는 점이 설명이 되더라구여. 여기서 또하나 궁금한 점은 client_with_auth 픽스처보다 host_user_calendar 픽스처가 먼저 실행이 되어야 공유되는 host_user 객체가 calendar 정보를 가질 수 있다는 점이었습니다. 이 실행 순서를 결정하는 프로세스에 대해서도 궁금합니다!
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 2회차 과제 질문
작가님, 4주 2회차 과제 내용이 잘 이해가 되지 않습니다 ^^;책 309쪽의 코드에서#이미 존재하는 타임슬롯과 겹치는지 확인 이 부분 에서 이미 과제에서 요구하는 사항이 구현된 것이 아닌가요?
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 1회차 과제
사용자가 캘린더를 보유했는지 여부로 확인할 것 같습니다. 이유는 추후 서비스 확장할 때 조금 더 수월할 것 같다는 생각이 들었습니다.프로젝트를 시작하면서부터 아래와 같은 생각을 했습니다.- "누구나 호스트가 되면 안될까?" - "호스트 역시도 커피챗을 신청할 수 있게 하고 싶다." 그래서 위와 같이 서비스 정책을 확장할 때를 대비해서 (마이그레이션 비용을 줄이는 방향으로) is_host로 검사하는 것이 아닌 일단 캘린더 보유 여부로 호스트인지 아닌지 확인하자라고 판단했습니다.
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 1회차 과제
두 요소를 모두 사용 하나, 각각 다른 용도로 쓸 것입니다. is_host 용도: 호스트 전용 엔드포인트 접근 통제 및 UI 메뉴 노출, 권한 기반 라우팅 호스트를 위한 endpoint API 에 대한 이용 허가 여부를 결정하는 호스트의 access token 생성 및 엔드포인트 함수 내의 조건문을 위해 필요한 모델 필드라고 생각합니다. 예상되는 우려점은 호스트 유저가 더이상 host가 되지 않을 때, host를 위한 API의 이용 허가를 거부 해야하기 때문에 is_host 필드값의 확실한 업데이트가 필요합니다. 사용자 캘린더 보유 여부 용도: 실제 예약 생성/동기화가 필요한 시점에 필수 체크, 호스트의 온보딩 플로우의 완료조건 게스트의 성공적인 Booking 을 위해서는 실제 캘린더에 대한 접근이 필요하기 때문에, 호스트는 게스트가 Booking 하기 위해서는 캘린더를 무조건 보유해야 합니다. 예상되는 우려점은 호스트의 캘린더는 호스트가 Booking을 받을 준비가 되있다면 항상 존재 해야하며, 실제 올바른 호스트에 매핑이 되어야합니다. 만약 캘린더가 존재 하지않거나, 호스트 캘린더 ID 에 대한 검증이 제대로 동작 하지 않는다면, 게스트의 Booking 이 실패로 이어지거나 다른 호스트 캘린더에 등록이 될 수 있습니다.
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 3회차 과제
중복 예약을 데이터베이스의 고유값 제약으로 예방하는 방법조건은 1)동일 일자와 동일 타임 슬롯인 경우 중복으로 간주 2)약속(부킹)이 취소되어 attendance_status 모델 필드값이 cancellled인 경우 예약 가능 위를 구현하기 위해 데이터베이스에게 아래 정보가 고유해야 한다는 특별한 규칙을 알려줄 수 있습니다.1)누가 예약하는지2)언제 예약하는지3)어느 시간대에 예약하는지 위의 세가지가 모두 같으면 중복 예약이라고 판단 합니다. 취소된 재예약은 가능하게 하기 위해고유성 규칙에 한가지 예외를 부여합니다.호스트, 날짜, 시간 슬롯의 조합은 고유해야 한다. 예외적으로 예약상태가 "취소됨"이라면 그 예약은 위의 고유성 규칙을 무시해 달라고 합니다. 새로운 예약 생성 시:데이터베이스는 예약하려는 호스트, 날짜, 시간슬롯 정보를 보고, 현재 '취소됨' 상태가 아닌 다른 예약이 이미 있는지 찾아봅니다.만약 '취소됨' 상태가 아닌 다른 예약이 이미 같은 자리에 있다면, 데이터베이스는 "안돼! 이미 예약된 자리야!"라고 하며 새로운 예약을 저장하지 못하게 합니다.만약 같은 자리에 '취소됨' 상태가 아닌 예약이 없다면, 데이터베이스는 새로운 예약을 성공적으로 저장합니다.예약 취소 시:어떤 예약이 '취소됨(cancelled)' 상태로 변경되면, 데이터베이스의 이 "특별한 규칙"은 그 취소된 예약을 더 이상 고유성 검사 대상에서 제외합니다.이제 그 자리는 "비어있는" 것처럼 간주되므로, 다른 사람이 같은 호스트, 날짜, 시간슬롯 정로 새로운 예약을 만들 수 있게 됩니다.
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 1회차 과제
사용자가 호스트인지 두 가지 방법으로 확인 가능합니다. 1. User 모델에 선언한 is_host 모델 필드가 True인지 확인 2.사용자가 캘린더를 보유했는지 확인 이 중 두 가지 방법 모두를 사용해야 한다고 생각합니다.1. User 모델에 선언한 is_host 모델 필드가 True인지 확인 -> 관점적 측면에서 명시적이고 관리적인 역할 및 자격을 부여합니다. 즉 사용자가 호스트로 자격이 있다는 것을 의미합니다. 관리자가 특정 사용자를 호스트로 지정하거나 해제하는데 사용할 수 있고 캘린더의 유무와 별개로 관리가 가능하게 서비스의 효율을 향상시켜 줍니다. 2.사용자가 캘린더를 보유했는지 확인->실제 예약 요청이 들어 왔을 때 해당 호스트가 유효한 캘린더를 보유하고 있는지 확인하는 최종적인 역할을 할 수 있습니다. 즉 위의 두 정보가 모두 필요한 이유는 존재하지 않는 사용자의 username으로 캘린더 정보를 가져오려고 하거나 호스트가 아닌 사용자의 username으로 캘린더 정보를 가져오려고 하는 시도를 막을 수 있을것으로 예상됩니다. 요약하면 is host는 "호스트로서의 신분"을, 캘린더 보유 여부는 "호스트로서의 활동을 나타내는 정보여서 모두 필요합니다.
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 3회차 과제
데이터베이스 고윳값 제약으로 예방하는 구현에 대해서 조사했습니다. 동일 일자와 동일 타임슬롯인 경우 중복Booking에 unique index를 생성하는 것인데 동일 일자 (when)과 동일 타임슬롯(time_slot_id) 조건을 생성하면 됩니다. CREATE UNIQUE INDEX uq_booking_slot ON bookings (when, time_slot_id);class Booking(SQLModel, table=True): __tablename__ = "bookings" __table_args__ = ( UniqueConstraint("when", "time_slot_id", name="uq_booking_slot"), ) attendance_status 필드값이 cancelled인 경우PostgreSQL의 경우 partial unique index가 가능합니다. where를 사용해서 field가 cancelled가 아닌 경우에 unique 제약을 걸면 됩니다. CREATE UNIQUE INDEX uq_booking_slot ON bookings (when, time_slot_id) WHERE attendance_status <> 'cancelled' ;class Booking(SQLModel, table=True): __tablename__ = "bookings" __table_args__ = ( UniqueConstraint("when", "time_slot_id", name="uq_booking_slot"), postgresql_where=text("attendance_status <> 'cancelled'") ) 다중 컬럼에 대한 unique는 종종 사용하는 방법이었는데 where를 이용해서 조건을 설정할 수 있는 건 이번에 첨 알았습니다. 필요하면 python에서 처리하곤 했었는데 데이터베이스 기능으로 이용하는게 레이스 컨디션도 피할 수 있고 장점이 있어보이네요. 실무에 적용할때도 꼭 이용해보겠습니다.
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 5회차 과제
파일 삭제 정책booking을 삭제할때 해당 booking과 업로드된 파일을 같이 삭제하는 정책의미없는 파일이 남아있는 것은 저장 공간 낭비유출 위험을 줄여준다(?) (예: 삭제된 booking의 파일을 남겨두어 총 100개의 파일이 저장된 상태라고 가정. 그 중에 70개는 필요한 파일, 나머지 30개는 필요없는(삭제된 booking) 파일임. 파일을 바로 제거하지 않아서 100개가 다 유출되는 것보다 의미없는 파일을 바로바로 제거하여 70개만 유출되는 것이 더(?) 낫다..)그러나 한편으로는 트래픽이 적은 시간에 삭제된 booking의 파일을 주기적으로 정리하는 로직을 실행하는 것이 더 나을까 하는 생각도 드네요
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 1회차 과제
is_host / 캘린더 보유 여부 중에 어느 것을 사용할까? 둘 다?is_host 는 반드시 사용할 것 같음is_host 필드가 있으면 캘린더 라는 다른 엔티티에 의존하지 않고 is_host 만으로 호스트 여부를 판단할 수 있어서캘린더 보유 여부는 선택적일 것 같음아래와 같은 정책을 설정해야할 필요가 있을때is_host 인데 캘린더가 있는 경우s_host 인데 캘린더가 없는 경우 아니면 '호스트는 최소 하나 이상의 캘린더는 있어야 한다' 같은 정책
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 1회차 과제
기획의 관점에서 Host 체크 방법is_host 필드 체크사용자가 호스트인지 먼저 확인을 하게 되므로 호스트 등록, 호스트만 조회, 호스트에 주어진 권한 제어 등 호스트를 따로 관리하고 싶을때 장점이 있습니다캘린더 보유 체크캘린더로 호스트일 경우는 호스트, 게스트 구분이 회원 등급이나 종류가 아닌 모두가 호스트나 게스트가 될수 있고 예약이 서로에게 일어날 수 있을때 장점이 있습니다. 구글 캘린더로 예를 들면 모두가 초대가 가능하고 초대도 가능한 방식입니다. 내가 선택한다면저라면 기본적으로는 is_host 필드로 체크하고 실제 예약을 받는다면 캘린더 체크를 이후에 하겠습니다. 호스트 관리를 따로 하게 되면 호스트만 따로 조회도 가능하고 호스트를 하고 싶지 않을때는 호스트가 되지 않는 것도 가능합니다. 또한 is_host를 기본 true로 하게 되면 모두가 호스트가 되는 방식도 가능하므로 앞에서 가정한 모두가 호스트/게스트인 방식도 쉽게 전환이 가능합니다. 예약 가능한 캘린더 체크는 실제로 이사람이 호스트라고 하더라도 예약일자가 있는지에 따라 예약화면 컨트롤 등이 가능하기 때문에 체크가 필요합니다. 요약하면 is_host 체크를 할 때 상황에 따라 유연하게 호스트 <-> 게스트 전환이 쉽고 필요한 경우 호스트이면서 게스트인 정책도 가능하기 때문에 좀 더 유연한 방법이라고 생각합니다.
-
미해결[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 4회차 과제 제출
쟁점 정리예약 일정 변경과 관련해서 결정해야 할 핵심은 다음과 같습니다:일자/타임슬롯 변경을 허용할 것인가? vs 취소 후 재예약만 가능하게 할 것인가?변경을 허용한다면, 언제까지 허용할 것인가?당일 변경은 불가능하다 (이건 확정)내가 선택한 정책일자/타임슬롯 변경을 허용하되, 예약일 24시간 전까지만 가능근거:"취소 → 재예약" 방식은 사용자가 불편하고, 원하는 시간이 이미 찼을 수도 있음24시간 전까지는 게스트에게 충분한 유연성을 주면서도, 호스트가 하루 전부터 안정적으로 준비할 수 있음규칙이 단순해서 사용자가 이해하기 쉽고, 구현도 간단함구현 시나리오시나리오 1: 여유 있는 변경 (성공)민수는 2월 10일 오후 2시를 예약했습니다.2월 7일에 2월 11일 오후 4시로 바꾸고 싶어졌습니다.→ 변경 가능 (24시간 이상 여유)→ "변경이 완료되었습니다."시나리오 2: 임박한 변경 시도 (실패)지영은 2월 10일 오후 2시를 예약했습니다.2월 9일 오후 3시에 시간을 바꾸려고 합니다.→ 변경 불가 (24시간 미만)→ "예약일 24시간 전까지만 변경 가능합니다. 취소 후 재예약해주세요."시나리오 3: 당일 변경 시도 (실패)현우는 2월 10일 오후 2시를 예약했습니다.2월 10일 오전 11시에 오후 4시로 바꾸려고 합니다.→ 변경 불가 (당일)→ "당일 일정 변경은 불가능합니다."이상입니다.
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 1회차 과제 제출합니다.
저는 is_host 필드와 calendar 존재 여부를 둘 다 검증해야 한다고 판단했습니다.왜 두 가지를 모두 사용해야 할까요?우선 두 요소는 서로 다른 의미를 가지고 있다고 생각합니다.is_host: 이 사용자가 호스트 자격이 있는가? (역할)calendar 존재: 이 호스트가 실제로 운영 중인가? (상태)현실로 비유하자면, is_host는 사업자등록증을 가지고 있는 상태이고, calendar가 있다는 건 실제로 가게를 오픈한 상태라고 볼 수 있습니다. 사업자등록증은 있지만 아직 가게를 열지 않은 사람도 있을 수 있잖아요?그래서 둘 중 하나만 체크하면 문제가 생긴다고 봅니다.만약 calendar 존재 여부만 체크한다면?calendar만 보고 판단하면 권한 체계가 무너질 수 있습니다. 예를 들어 일반 사용자가 억지로 캘린더를 생성하면 호스트로 둔갑할 수 있는 보안 문제가 생기죠. 또 호스트가 일시적으로 캘린더를 삭제했다가 다시 만들 때마다 권한 관리가 복잡해집니다.만약 is_host만 체크한다면?반대로 is_host만 체크하면, 캘린더가 없는데도 예약 관련 API를 호출할 수 있게 되어서 500 에러가 발생할 수 있습니다. "호스트인데 캘린더는 없는" 이상한 상태를 어떻게 처리해야 할지도 애매해지고요.두 가지를 순차적으로 검증하는 게 좋습니다저는 이렇게 단계적으로 확인하는 게 맞다고 봅니다:먼저 is_host로 호스트 자격이 있는지 확인그 다음 calendar 존재로 실제 운영 가능한지 확인이렇게 하면 사용자에게도 명확한 피드백을 줄 수 있어요. "호스트 권한이 없습니다" vs "캘린더를 먼저 생성해주세요" 처럼 무엇을 해야 하는지 정확히 알려줄 수 있죠.미래 확장성도 고려했습니다나중에 서비스가 커지면 호스트 상태를 더 세분화할 수도 있을 것 같아요:준비 중 호스트 (is_host=true, 캘린더 없음) → 대시보드 접근만 가능운영 중 호스트 (is_host=true, 캘린더 있음) → 모든 기능 사용 가능휴면 호스트 (is_host=true, 캘린더 비활성화) → 기존 예약 조회만 가능이런 식으로 관리하려면 역할과 리소스를 분리해서 체크하는 게 필수라고 생각합니다.성능 걱정은 없을까요?is_host는 User 모델의 필드라서 추가 DB 쿼리가 필요 없고, calendar 조회도 어차피 해야 하는 작업이라 성능에 큰 영향은 없다고 봅니다.
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 2회차 과제
안녕하세요. 과제를 잘 못 이해한건가 싶기도 하지만 아래와 같은 조건하에 구현을 해 봤습니다. 수업 과정에서 Front-end는 고정인 상황이기 때문에 기존 API는 변경이 없다는 가정하에 변경 해 봤습니다. 시간대1개와 요일n개의 설정이 하나의 row로 묶여있는 상태로는 요일별 시간대 변동에 대응하기 까다로운 것 같아 요일별 개별 row로 저장하도록 일부 수정 하였습니다. API endpoint 모델 변경을 피하기 위해서 내부적으로 여러개의 요일별 row로 나누어 저장하도록 했습니다. 고민중에 작가님께서 올려 놓으신 레퍼런스 코드를 봤는데, 책에서 언급하신 것 처럼 postgres 경우와 sqlite 경우로 코드가 분기 되는 것을 보았고, 지금은 교육 과정이기 때문이라고 생각 하지만 배포 코드와 개발 환경 코드가 다른것은 여러모로 좋지 않은 것 같아 현재는 sqlite 기준으로 개별 DB구현에서만 지원하는 것은 배제하는 방향으로 구현 했습니다. 1과 같이 변경 함으로써, 기 등록된 정보의 수정에서는 개별 날짜 별 시간대 설정 면에서 자유도가 생겼다고 생각 됩니다.타임슬롯의 변경과 삭제에 관해서는 내부적으로는 time-slot으로 관리하지만, 외부공개 id는 아니므로, 키 로서 start-time, end-time, weekday (API형태로서는 리스트)조합으로 정의 해서 동작 하도록 구현했습니다.특히 삭제의 경우는 부득이 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, )
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
patch 요청시 payload가 넘어가지 않습니다.
아래 두 요청 코드에서 patch 요청 시 payload가 엔드포인트 함수에서 None으로 잡혀 model_dump()에서 오류가 발생합니다. 원인을 잘 모르겠습니다.[오류][요청 코드]@pytest.mark.parametrize("payload", [ {"display_name": "푸딩캠프"}, {"email": "hannal@example.com"}, {"display_name": "푸딩캠프", "email": "hannal@example.com"}, ]) async def test_사용자가_변경하는_항목만_변경되고_나머지는_기존_값을_유지한다( client_with_auth: TestClient, # 인증을 받은 클라이언트 payload: dict, # 클라이언트 요청 페이로드 host_user: User, # 클라이언트 사용자 ): # 현재 사용자 정보를 보관한다. before_data = host_user.model_dump() response = client_with_auth.patch("/account/@me", json=payload) # (...)async def test_비밀번호_변경_시_해싱_처리한_비밀번호가_저장되어야_한다( client_with_auth: TestClient, host_user: User, db_session: AsyncSession, ): before_data = host_user.hashed_password payload = { "password": "new_password", "password_again": "new_password", } response = client_with_auth.patch("/account/@me", json=payload) # (...)[엔드포인트]@router.patch("/@me", response_model=UserDetailOut) async def update_user( user: CurrentUserDep, session: DbSessionDep, payload: UpdateUserPayload = Body(...), ) -> User: updated_data = payload.model_dump(exclude_none=True, exclude={"password", "password_again"}) stmt = update(User).where(User.id == user.id).values(**updated_data) await session.execute(stmt) await session.commit() await session.refresh(user) return user [스키마]class UpdateUserPayload(SQLModel): display_name: str | None = Field(default=None, min_length=4, max_length=40) email: EmailStr | None = Field(default=None, max_length=128) password: str | None = Field(default=None, min_length=8, max_length=128) password_again: str | None = Field(default=None, min_length=8, max_length=128) @model_validator(mode="after") def check_all_fields_are_none(self) -> Self: if not self.model_dump(exclude_none=True): raise ValueError("최소 하나의 필드는 반드시 제공되어야 합니다.") return self @model_validator(mode="after") def verify_password(self) -> Self: if self.password is not None or self.password_again is not None: # 둘 중 하나라도 들어오면 둘 다 있어야 함 if not self.password or not self.password_again: raise ValueError("비밀번호 변경 시 password와 password_again을 모두 제공해야 합니다.") if self.password != self.password_again: raise ValueError("비밀번호가 일치하지 않습니다.") @computed_field @property def hashed_password(self) -> str | None: if self.password: return hash_password(self.password) return None[픽스처]@pytest.fixture(autouse=True) async def db_session(): dsn = "sqlite+aiosqlite:///:memory:" engine = create_async_engine(dsn) async with engine.begin() as conn: await conn.run_sync(SQLModel.metadata.drop_all) await conn.run_sync(SQLModel.metadata.create_all) session_factory = create_session(engine) async with session_factory() as session: yield session await conn.run_sync(SQLModel.metadata.drop_all) await engine.dispose() @pytest.fixture() def fastapi_app(db_session: AsyncSession): app = FastAPI() include_routers(app) async def override_use_session(): yield db_session app.dependency_overrides[use_session] = override_use_session return app @pytest.fixture() async def host_user(db_session: AsyncSession): user = account_models.User( username="puddingcamp", hashed_password=hash_password("testtest"), email="puddingcamp@example.com", display_name="푸딩캠프", is_host=True, ) db_session.add(user) await db_session.flush() await db_session.commit() return user @pytest.fixture() def client_with_auth(fastapi_app: FastAPI, host_user: account_models.User): payload = LoginPayload.model_validate({ "username": host_user.username, "password": "testtest", }) with TestClient(fastapi_app) as client: response = client.post("/account/login", json=payload.model_dump()) assert response.status_code == status.HTTP_200_OK auth_token = response.cookies.get("auth_token") assert auth_token is not None client.cookies.set("auth_token", auth_token) yield client
-
미해결FastAPI 완벽 가이드
Update(수정)용 모델 질문 드립니다.
안녕하세요 교수님강의를 통해 많은 도움을 받고 있는 수강생입니다.수정 기능 구현 시, 전체 데이터를 Body에 담아 보내는 방식은 대용량 파일이나 멀티파트 폼 데이터 처리 시 효율성이 떨어지는 경험을 했습니다. 그래서 모든 필드를 Optional로 설정하여 부분 수정을 허용하게 하면 벨리데이션(Validation) 로직이 복잡해지는 문제가 발생했습니다. 현업에서는 어떤 식으로 업데이트 코드 스타일을 관리하는 것이 표준인지 교수님의 조언을 듣고 싶습니다.감사합니다.
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 1회차 과제
- is_host를 기준으로 호스트 사용자와 게스트 사용자를 구분하는 것은, 가입 당시부터 본인의 역할을 선택하여 구분 짓는 경우에 효과적인 것으로 판단됩니다. 가입 시점부터 역할(게스트/호스트)을 구분 지어서 관리하고자 하는 경우에 적절한 구조라고 생각되며, 호스트의 자격조건이 있거나, 검증이 필요한 서비스라면 이렇게 관리하는 방법이 적절해 보입니다. - 반면, 호스트/게스트의 타입을 캘린더의 존재 여부(또는 갯수)로 정의한다고 하면, 모든 가입자가 가입시에 동일한 자격을 갖는 가입자로서 가입처리가 되고, 추가 단계로서 캘린더를 생성 함으로 써, 호스트와 게스트의 역할을 자유롭게 넘나드는 자유도가 생기는 구조가 될 것 같습니다. - 지금은 커피챗을 목적으로 일정을 조율하는 목적을 갖는 시스템이므로, is_host를 사용하지 않는 결정을 하겠습니다. 전자상거래 시스템같은 판매자와/소비자 처럼 엄격한 구분과 자격을 검증해야 하는 시스템으로 여겨지지는 않기때문입니다. is_host 필드가 없어지는 경우에, 사용자의 상태(또는 역할)을 구분하기 위해서 매번 캘린더의 존재 여부를 확인해야 하지만 앞서 언급한 호스트와 게스트의 역할 변경에 열려있는 점이 중요하게 생각되기 때문입니다. 하지만 모든 가입자가 게스트로 시작하는 만큼 커피챗 호스트를 어떻게 유치할 것인가는 고민이 되는 부분입니다.
-
미해결Complete Full-Stack Python Developer Roadmap: Learn FastAPI, React, Database Design API Architecture
강의 업데이트에 대해 문의드립니다.
안녕하세요.커리큘럼을 봐도 현재 업데이트된 강의 시간내에 전부 커버가 안되고 있는 것으로 보이는데,강의는 어느정도 길이로 계획되어있고, 언제 업데이트가 전부 완료될지 예상 시점이 궁금하여 문의드립니다.
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
페이지 144 코드 문의
def create_engine(dsn: str): return create_async_engine( dsn, echo=True, # SQLAlchemy가 실행하는 SQL을 콘솔에 출력하도록 함 ) def create_session(async_engine: AsyncEngine | None = None): if async_engine is None: async_engine = create_engine()위 코드중 마지막줄async_engine = create_engine() 여기에 dsn 인자가 없어서 오류 나지 않나요?
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
책과 github 코드가 다릅니다 p130
책에서는 128pif TYPE_CHECKING: from appserver.apps.calendar.models import Calendar이렇게 appserver 부터 시작하니다 130페이지 하단에 보면https://gilbut.co/c/25069573YP커밋 참조하라고 되어있는데요깃헙에서는 if TYPE_CHECKING: from apps.calendar.models import Calendar여기는 apps부터 시작합니다. 초보에게는 이런거 하나하나가 어렵네요어떤게 맞는 건지 궁금합니다.