diff --git a/app.py b/app.py index cd5047f..e3b98d6 100644 --- a/app.py +++ b/app.py @@ -487,7 +487,14 @@ def get_schedule_preview_endpoint(preview_id: UUID) -> SchedulePreviewResponse: def create_schedule_endpoint( payload: ScheduleCreateRequest | SchedulePreviewScheduleRequest, idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"), + auth_user_id: int | None = Header(default=None, alias="X-Auth-User-Id"), ) -> ScheduleResponse: + """일정을 만든다. + + X-Auth-User-Id 는 Spring 이 검증한 액세스 토큰에서 꺼낸 소유자다. 외부에서 직접 + 들어오는 값이 아니라 같은 도커 네트워크 안의 Spring 만 보낸다. 헤더가 없으면 + 소유자 없는 일정으로 저장한다. 인가를 아직 켜지 않아 비로그인 생성이 가능하다. + """ started_at = monotonic() if idempotency_key: if not isinstance(payload, SchedulePreviewScheduleRequest): @@ -502,6 +509,7 @@ def create_schedule_endpoint( create_request, preview_id=preview.preview_id, fixed_events_by_day=fixed_events_by_day, + owner_id=auth_user_id, ) attach_schedule_to_preview(preview.preview_id, schedule.id) log.info( @@ -519,7 +527,7 @@ def create_schedule_endpoint( payload.start_date, payload.end_date, ) - schedule = create_schedule(payload) + schedule = create_schedule(payload, owner_id=auth_user_id) log.info( "direct schedule created. scheduleId=%s, elapsedMs=%d", schedule.id, diff --git a/schedule/persistence.py b/schedule/persistence.py index a1f4997..ddc2601 100644 --- a/schedule/persistence.py +++ b/schedule/persistence.py @@ -178,7 +178,17 @@ def mark_preview_consumed(preview_id: UUID) -> None: conn.commit() -def save_schedule(schedule: ScheduleResponse, condition_request: ScheduleCreateRequest) -> None: +def save_schedule( + schedule: ScheduleResponse, + condition_request: ScheduleCreateRequest, + owner_id: int | None = None, +) -> None: + """일정을 저장한다. + + owner_id 는 Spring 이 검증한 토큰에서 꺼내 X-Auth-User-Id 헤더로 전달한 값이다. + 한 번 정해진 소유자는 바꾸지 않는다. 수정 요청은 소유자를 싣지 않으므로, + 덮어쓰면 첫 수정에서 주인이 사라진다. + """ with connect() as conn: with conn.cursor() as cur: place_ids = sorted({stop.place.id for day in schedule.days for stop in day.stops if stop.place.id is not None}) @@ -196,12 +206,12 @@ def save_schedule(schedule: ScheduleResponse, condition_request: ScheduleCreateR id, status, start_date, end_date, daily_start_time, daily_end_time, start_place_name, start_longitude, start_latitude, end_place_name, end_longitude, end_latitude, preview_id, time_zone, lodging_mode, route_coverage, planning_warnings_json, - style_summary, condition_json, created_at, updated_at + style_summary, condition_json, user_id, created_at, updated_at ) VALUES ( %(id)s, %(status)s, %(start_date)s, %(end_date)s, %(daily_start_time)s, %(daily_end_time)s, %(start_place_name)s, %(start_longitude)s, %(start_latitude)s, %(end_place_name)s, %(end_longitude)s, %(end_latitude)s, %(preview_id)s, %(time_zone)s, %(lodging_mode)s, %(route_coverage)s, %(planning_warnings_json)s, - %(style_summary)s, %(condition_json)s, COALESCE((SELECT created_at FROM schedules WHERE id = %(id)s), %(now)s), %(now)s + %(style_summary)s, %(condition_json)s, %(user_id)s, COALESCE((SELECT created_at FROM schedules WHERE id = %(id)s), %(now)s), %(now)s ) ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, @@ -222,6 +232,9 @@ def save_schedule(schedule: ScheduleResponse, condition_request: ScheduleCreateR planning_warnings_json = EXCLUDED.planning_warnings_json, style_summary = EXCLUDED.style_summary, condition_json = EXCLUDED.condition_json, + -- 이미 주인이 있으면 유지한다. 수정 요청은 소유자를 싣지 않아 + -- 그대로 덮으면 첫 수정에서 NULL 이 된다. + user_id = COALESCE(schedules.user_id, EXCLUDED.user_id), updated_at = EXCLUDED.updated_at """, { @@ -237,6 +250,7 @@ def save_schedule(schedule: ScheduleResponse, condition_request: ScheduleCreateR "end_place_name": schedule.days[-1].end_location.name if schedule.days and schedule.days[-1].end_location else None, "end_longitude": schedule.days[-1].end_location.longitude if schedule.days and schedule.days[-1].end_location else None, "end_latitude": schedule.days[-1].end_location.latitude if schedule.days and schedule.days[-1].end_location else None, + "user_id": owner_id, "preview_id": schedule.preview_id, "time_zone": schedule.planning_assumptions.time_zone if schedule.planning_assumptions else "Asia/Seoul", "lodging_mode": schedule.planning_assumptions.lodging_mode if schedule.planning_assumptions else "UNSPECIFIED", diff --git a/schedule/service.py b/schedule/service.py index be5ed88..6d7c9ce 100644 --- a/schedule/service.py +++ b/schedule/service.py @@ -396,6 +396,7 @@ def create_schedule( request: ScheduleCreateRequest, preview_id: UUID | None = None, fixed_events_by_day: dict[int, list[FixedEventSpec]] | None = None, + owner_id: int | None = None, ) -> ScheduleResponse: log.info( "schedule build started. previewId=%s, startDate=%s, endDate=%s, candidateSource=%s", @@ -465,7 +466,7 @@ def create_schedule( saved = STORE.save(schedule) if db_enabled(): try: - save_schedule_to_db(saved, condition_request=request) + save_schedule_to_db(saved, condition_request=request, owner_id=owner_id) except Exception: log.exception("schedule persistence failed. scheduleId=%s", saved.id) log.info(