묻고 답해요
167만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <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, )
-
해결됨Flutter로 웹 서비스 개발하기 (1) - Responsive Web
SEO에 대해
안녕하세요.우선 강의 정말 잘 수강했습니다. https://pub.dev/packages/flutter_seo해당 커뮤니티를 보다가 댓글에서 해당 패키지를 찾게되었고강사님도 해당 seo패키지를 사용해서 seo적용을 하신것같은데,지금 배포된 샐링잇 웹사이트도 해당 seo패키지만을 적용해서 배포를 하신건지 궁금합니다. 그러면 기존 웹사이트 seo만큼은 아니지만, 저 패키지를 적용하면 회사이름(고유명사) 정도는 구글에 치면 나올 정도로는 적용을 할 수 있는건지 궁금합니다.
-
미해결쉽게 설명하는 AWS 기초 강의
개인 블로그에 올려도되나요?
안녕하세요혹시 강의 PDF 일부 캡처본(1~2개정도)를 학습 기록용 블로그에 올려도 되는지 궁금합니다
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <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
-
미해결비전공자도 이해할 수 있는 Nginx 입문/실전
HTTPS 관련 코드 해석하기
안녕하세요. 섹션 7의 "35. Nginx, Certbot이 작성한 HTTPS 관련 코드 해석하기" 강의를 들으면서 궁금한 점이 생겨서 질문 남깁니다. server { # 2. 들어온 요청의 주소의 Host가 jscode.p-e.kr일 경우 (ex. http://jscode.p-e.kr/about) # https://jscode.p-e.kr/...(ex. https://jscode.p-e.kr/about)로 리다이렉트(301) 처리 if ($host = jscode.p-e.kr) { return 301 https://$host$request_uri; } # managed by Certbot # 1. jscode.p-e.kr 주소로 들어온 요청이면서 # 80번 포트(http)로 들어오는 요청일 때 # 이 server 블럭에서 처리하도록 설정 listen 80; server_name jscode.p-e.kr; # 3. 그 이외의 경우에는 404(Not Found)로 응답 return 404; # managed by Certbot } 강의자료에서 제공해주시는 /etc/nginx/conf.d/default.conf 파일의 일부 코드입니다.이미 server_name에서 jscode.p-e.kr 주소로 들어온 요청인지 확인하니깐, if( $host = jscode.p-e.kr) 는 없어도 되는거 아닌가요? 코드가 존재하는 다른 의도가 있는건지 궁금합니다.
-
해결됨CloudNet@ - Amazon EKS 기본 강의
강의 연장 부탁드립니다.
안녕하세요. 해당 강의 수강할 수 있는 기간이 있던 걸로 기억해서 확인해보니 작년 2월 3일에 구매했더라구요.아직 완강하지 못해서 강의 연장을 요청 드립니다. 확인 부탁드리겠습니다.감사합니다.
-
미해결비전공자도 이해할 수 있는 Nginx 입문/실전
default.conf 파일이 없습니다.
/etc/nginx/conf.d 폴더에 default.conf 파일이 없습니다.
-
미해결스프링 부트와 리액트로 구현하는 보안 JWT 로그인
섹션 9 실전 질문
강사님 세션 9에서 Secretes Manager나 Parameter Store 를 현업에서도 깃허브 액션 하고 같이 쓰나요?이거 궁금해서 질문드립니다.
-
미해결Golang을 통한 백엔드 개발 및 환경 구축하기
프로젝트 구조 관련 질문이 있습니다.
안녕하세요!! 클라우드 관련 공부하다가 go에 관심이 생겨 수강하게 되었습니다.백엔드는 이 강의와 책으로 처음 공부하는 중입니다.현재는 강의 프로젝트 구조로 개인 프로젝트 진행해보려고 리펙터링 중 입니다. 수업과 관련은 없지만 3가지 질문이 있습니다.1. 트랜잭션 구현을 위한 *sql.DB는 어디서 생성하고 어떻게 service에 주입해야 하나요?reposiory는 수정해서, *sql.DB 필드 하나만 위치하도록 했습니다. 다만, 트랜잭션 구현을 하다보니, db 커넥션 풀을 repository에서 service로 가져오거나, cmd에서부터 service까지 내려보내주어야 하더라고요. cmd에 db 커넥션 풀을 생성하도록 하면 안되는 걸까요? orm 도입하기 이전에 내장 패키지로 기본 로직을 구현하려는 건 쓸 데 없는 일일까요?2. 프로젝트 구조를 어째서 cmd에 전부 주입하는 식으로 작성하셨나요? 테스트는 어떻게 하시나요?cmd를 main패키지 처럼 사용하시는 건지 궁금합니다. 제가 알고 있던 백엔드 구조(spring boot)와 많이 달라 보이네요. 또, 이 구조에서 테스트는 어떻게 진행해야 할지 잘 모르겠습니다. 대략적인 흐름이라도 알고자 여쭤봅니다.3. 일하실 때에도 이 구조를 유지하시나요? 도메인별 패키지 분리(user, auth, member... etc)는 별로인가요?개인적으로는 비즈니스 도메인별로 디렉터리를 나누는 것을 선호합니다. 다만, 현재 이 디렉터리 구조를 뜯어고칠 용기가 생기지는 않네요... 이 구조의 어떤 점 때문에 이렇게 사용하시는 지, 혹은 팀에서 이렇게 쓰길래 유지를 하고 계신건지 궁금합니다. 장단점이 있을까요?궁금한걸 쌓아두면서 gpt로 해결하려다가, 쌓아둔게 터진 것처럼 장황하네요.. 강의를 들으면서 해결 못한 내용이라 그런 것 같아요. 좋은 강의 잘 듣고 있습니다 🥹🙏
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
4주 1회차 과제
- is_host를 기준으로 호스트 사용자와 게스트 사용자를 구분하는 것은, 가입 당시부터 본인의 역할을 선택하여 구분 짓는 경우에 효과적인 것으로 판단됩니다. 가입 시점부터 역할(게스트/호스트)을 구분 지어서 관리하고자 하는 경우에 적절한 구조라고 생각되며, 호스트의 자격조건이 있거나, 검증이 필요한 서비스라면 이렇게 관리하는 방법이 적절해 보입니다. - 반면, 호스트/게스트의 타입을 캘린더의 존재 여부(또는 갯수)로 정의한다고 하면, 모든 가입자가 가입시에 동일한 자격을 갖는 가입자로서 가입처리가 되고, 추가 단계로서 캘린더를 생성 함으로 써, 호스트와 게스트의 역할을 자유롭게 넘나드는 자유도가 생기는 구조가 될 것 같습니다. - 지금은 커피챗을 목적으로 일정을 조율하는 목적을 갖는 시스템이므로, is_host를 사용하지 않는 결정을 하겠습니다. 전자상거래 시스템같은 판매자와/소비자 처럼 엄격한 구분과 자격을 검증해야 하는 시스템으로 여겨지지는 않기때문입니다. is_host 필드가 없어지는 경우에, 사용자의 상태(또는 역할)을 구분하기 위해서 매번 캘린더의 존재 여부를 확인해야 하지만 앞서 언급한 호스트와 게스트의 역할 변경에 열려있는 점이 중요하게 생각되기 때문입니다. 하지만 모든 가입자가 게스트로 시작하는 만큼 커피챗 호스트를 어떻게 유치할 것인가는 고민이 되는 부분입니다.
-
미해결비전공자도 이해할 수 있는 CI/CD 입문·실전
CodeDeploy 사용시 registration 요구
CodeDeploy 를 클릭했더니, 이런 문구가 뜨네요.complete your AWS registration 눌러도 콘솔 홈으로 돌아가고,credit card information 을 눌렀더니, 결제 기본 설정 창이 뜨는데, 딱히 문제될 건 없어 보여요.다른 AWS 서비스 사용할 때는 이런 문구 안 떴는데 왜 CodeDeploy만 이럴까요? ㅠㅠ
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <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 인자가 없어서 오류 나지 않나요?
-
미해결비전공자도 이해할 수 있는 CI/CD 입문·실전
스프링 종료 명령어
안녕하세요 sudo fuser -k -n tcp 8080 이 명령어로 스프링 죽이시던데 실제 실무에서도 저 명령어로 스크립트를 구성하나요?
-
해결됨실전! GitHub Actions으로 CI/CD 시작하기
자료가 깨진 것 같습니다.
자료를 다운받았는데, 압축 파일 안에 아무것도 뜨지 않습니다.확인해주시고 자료를 새롭게 업로드 해주시면 감사하겠습니다
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <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부터 시작합니다. 초보에게는 이런거 하나하나가 어렵네요어떤게 맞는 건지 궁금합니다.
-
미해결비전공자도 이해할 수 있는 AWS 입문/실전
영상이 안 나옵니다 ㅠㅠ
영상은 안 보이고 소리만 재생됩니다
-
미해결스프링 부트와 리액트로 구현하는 보안 JWT 로그인
깃허브 코드
강사님 혹시 44강 듣고 있는데 이거 프론트엔드 코드가 깃허브에 있는것과 다른 코드인데 어디서 볼 수 있나요?
-
미해결eks를 활용한 spring 운영서버 배포(feat. devops의 모든것)
rds에 db 인스턴스 크기가 없는데 어떤 걸 선택하면 될까요?
단일 AZ DB 인스턴스 배포를 선택하면 될까요?
-
해결됨[매일 완독 챌린지] 저자와 함께하는 <FastAPI로 기획에서 출시까지>
120페이지 코드 질문드립니다.
120 페이지 코드 보면 created_at 코드가class OAuthAccount 코드하고 들여 쓰기 레벨이 같은데 맞는건가요?
-
미해결비전공자도 이해할 수 있는 AWS 입문/실전
이번 실습은 무료 플랜에서도 가능한가요?
인증서 발급 시, 저 빨간 박스에는 "내가 미리 만든 도메인" 이름을 적어야 하는 것 맞나요?Route53에는 아무런 도메인도 안 만든 상태이고, 그래서 레코드 생성도 못해요. 그런데 도메인은 Route53에서 만드는 데 무료 플랜에선 불가능하잖아요.그래서 다음과 같이 내도메인.한국 사이트에서 커스텀 도메인과 HTTPS 인증서를 만들었어요. 그리고 커스텀 도메인 접속도 성공했어요. 그런데, 분명 인증서는 미국 동부(버지니아 북부)에서 발급 받아야 하는데, 저는 그런 설정 없이 성공했는데 그 이유가 무엇인가요? 아무런 문제 없나요?그리고 제가 잘못 알고 있는 부분이 있을까요?