아래는 기지국 기반 위성제어 시스템 + 촬영계획시스템 연계 + 위성정보 관리 + 부가서비스 확장을 전제로 한
인공위성(Satellite) 엔터티의 전체 속성 체계 정의안입니다.
단순 물리적 스펙이 아니라,
까지 포함하는 플랫폼 관점의 통합 속성 모델입니다.
위성은 단일 테이블로 끝내면 안 됩니다.
아래와 같은 도메인 분리형 모델로 설계하는 것이 적절합니다.
Satellite
├─ Identification
├─ Orbit & Dynamics
├─ Physical Bus
├─ Payload
├─ Communication
├─ Power
├─ ADCS (자세제어)
├─ Thermal
├─ Onboard Computer
├─ Mission Profile
├─ Imaging Capability
├─ Ground Interface
├─ Operation Status
├─ Data Product Capability
├─ Security
├─ Lifecycle
└─ Service Enablement
촬영계획 및 가시성 계산의 핵심 영역입니다.
촬영계획 시스템과 직결되는 핵심 영역
촬영계획시스템에서 반드시 필요한 속성
기지국 연동 핵심 영역
실시간 상태 관리
부가서비스 개발 관점 핵심
플랫폼 비즈니스 관점
실제 시스템 설계 시에는 위성을 단일 객체로 두지 않고 다음과 같이 분리하는 것이 바람직합니다:
→ 식별/기본 정보
→ 촬영/통신/기술적 스펙
→ 실시간 상태
→ 임무 정의
→ 촬영 요청 단위
→ 생성되는 영상 상품
→ 지상국 접속 세션
인공위성의 속성 정의는 단순한 스펙 나열이 아니라,
> “제어 가능성 + 임무 수행 능력 + 데이터 생산 능력 + 플랫폼 확장성”
을 모두 포함해야 합니다.
특히 귀하의 목적이
이라면,
위성은 단순 장비가 아니라
서비스 플랫폼의 핵심 자산(Asset Model) 로 정의되어야 합니다.
필요하다면 다음 단계로:
아래는 요청하신 4가지(1) ERD 논리 데이터 모델, (2) 마이크로서비스 분리, (3) 통합 플랫폼 아키텍처, (4) 촬영계획 최적화 알고리즘 구조를 순서대로, 그리고 전문가 수준(실제 구현을 염두에 둔 수준)으로 정리한 답변입니다.
전제는 “위성 자산(Asset) + 기지국 TT&C 제어 + 촬영계획(Tasking) + 영상상품(Data Product) + 부가서비스(상용 API)”입니다.
ERD를 “위성=한 테이블”로 만들면 운영/확장 시 반드시 터집니다. 최소 아래 7개 도메인으로 분리합니다.
아래 관계가 “촬영계획+제어+상품화”를 완성하는 핵심 골격입니다.
> 설계 포인트: “위성 성능/제약 값”은 변경이 잦고 히스토리가 필요합니다. 따라서 SATELLITE에 박지 말고 PROFILE(유효기간)로 관리합니다.
> 촬영계획/다운링크는 “가시구간”이 핵심이므로 VISIBILITY_PASS를 1급 엔터티로 둡니다(매번 계산하지 않고 캐싱/재사용).
> 설계 포인트: 촬영계획은 결국 “제약조건의 결합”입니다. 제약을 코드에만 두면 운영이 불가능해집니다. 제약을 데이터로 승격시키세요.
> 설계 포인트: “요청”과 “실행”은 분리해야 합니다(승인/스케줄/전송/ACK/재시도/감사추적 때문에).
> 설계 포인트: IMAGING_REQUEST는 “고객언어”, IMAGING_PLAN은 “계획언어”, SCHEDULE_SLOT은 “운영언어(시간축 예약)”입니다. 이 3단 분리를 안 하면 기능이 커질수록 복잡도가 폭발합니다.
VISIBILITY_PASS 제공ImagingRequestSubmittedPlanFeasibleComputedScheduleCommittedContactSessionCompletedProductAvailableDeliveryCompleted기지국/위성은 보통 표준이 제각각입니다. 그래서 Adapter/Connector 계층이 별도여야 합니다.
를 단일 이벤트 버스로 흘립니다.
이점
촬영계획은 전형적으로 제약최적화(Constraint Optimization) 문제입니다.
요구사항이 늘수록 “휴리스틱+정수계획+재계획”의 하이브리드가 실무적으로 가장 안정적입니다.
각 요청 r에 대해:
(r, s, pass, mode) 생성산출물: IMAGING_PLAN 후보군 + score feature
후보마다 점수:
> 실무 팁: 스코어는 “학습 가능(feature 기반)”로 설계하면 나중에 자동 튜닝이 쉬워집니다.
여기서 2가지 접근을 혼합합니다.
권장 운영 방식:
SCHEDULE_SLOT 확정 + 변경 이력 기록스케줄러 서비스 내부에는 보통 아래 구조가 필요합니다.
(질문 안 하셔도, 설계 품질을 좌우하는 핵심이라 같이 드립니다.)
원하시면, 위 4개 산출물을 더 “설계 문서 형태”로 만들어드릴 수 있습니다.
다음으로는 보통 위성 종류(광학/ SAR / 복수 위성군 여부) + 지상국 운영 형태(자체/외부/혼합)에 따라 최적 설계가 갈리는데, 일단 지금 답변은 “가장 일반적이고 확장 가능한 베이스라인”으로 구성했습니다.
좋습니다. 앞서 4개 항목을 “개념/구조” 중심으로 설명했으니, 이제는 실제 설계 산출물 수준(필드, API, 이벤트, 시퀀스, 최적화 수식 템플릿, 운영 시나리오)로 한 단계 더 내려가서 계속하겠습니다. 순서는 동일하게 1) ERD 상세 → 2) 마이크로서비스 상세 → 3) 통합 아키텍처 시나리오/보안/운영 → 4) 최적화 알고리즘(수식/휴리스틱/재계획) 입니다.
created_at, updated_at, (운영 이벤트는 event_time)PROFILE + effective_from/to로 히스토리 관리requested_by, approved_by, correlation_id, audit_log_refgeometry(Polygon, 4326) + GIST 인덱스satellite_id (UUID)name, norad_id (int, unique), cospar_id (varchar, unique)operator_id (FK), constellation_id (FK)orbit_type, mission_type, statuslaunch_date, eol_estimated_atunique(norad_id), unique(cospar_id)idx_satellite_statusprofile_set_idsatellite_id (FK)imaging_profile_id (FK), comm_profile_id (FK), constraint_profile_id (FK)effective_from, effective_to(satellite_id, effective_from desc) 최신 적용 빠르게> 프로파일을 위성에 직접 FK로 물리면 “시간 유효기간”이 깨집니다. 포인터 테이블로 묶는 방식이 운영에 강합니다.
imaging_profile_idsensor_type, mode_code, spectral_band_setgsd_m, swath_km, bit_depthmax_off_nadir_deg, slew_rate_deg_per_secmin_sun_elev_deg, night_support_flagstereo_flag, video_flag(sensor_type, mode_code) 조합 조회TLE(tle_id, satellite_id, epoch_time, line1, line2, ingested_at)EPHEMERIS_STATE_VECTOR(eph_id, satellite_id, time, x,y,z,vx,vy,vz, frame, ingested_at)TLE(satellite_id, epoch_time desc)EPHEMERIS(satellite_id, time)pass_idsatellite_id (FK), station_id (FK)aos_time, los_time, max_elevation_degpass_type (ttc/downlink/both)predicted_at, orbit_source_ref(satellite_id, aos_time)(station_id, aos_time)request_idtenant_id/customer_idaoi_geom (geometry polygon)time_window_start/endpriority (1..n), sla_due_timeproduct_level, max_cloud_pct, max_off_nadir_degstatus(submitted/validated/planned/scheduled/executed/failed/cancelled)GIST(aoi_geom)(status, priority, sla_due_time)(time_window_start, time_window_end)candidate_idrequest_id (FK), satellite_id (FK), pass_id (FK)mode_code, predicted_start/endscore_value, feasible_flag, infeasible_reason(request_id, score_value desc)(satellite_id, predicted_start)slot_idsatellite_id (FK)start_time, end_timeslot_type(imaging/downlink/maneuver/safe)linked_candidate_id (FK, nullable)state(planned/committed/executed/cancelled)version (낙관적 락), schedule_run_id (어떤 최적화 실행 결과인지)(satellite_id, start_time) + overlap 체크용(구현은 exclusion constraint 권장)(state, start_time)> PostgreSQL이면 (satellite_id, tsrange(start,end)) + EXCLUDE USING gist로 “겹침 금지”를 DB 차원에서 보장하는 설계가 강력합니다.
cmd_req_id, satellite_id, command_type, payload_jsonrequested_by, approved_by, priority, statuscmd_exec_id, cmd_req_id, session_id, sent_at, ack_at, result, raw_log_uri(satellite_id, requested_at desc)(status, priority)product_id, request_id, satellite_id, sensing_timefootprint_geom, cloud_cover_est, level, formatprocessing_status, uri, checksumdelivery_id, product_id, tenant_id, method, status, delivered_atGIST(footprint_geom)(tenant_id, sensing_time desc)SATELLITE*, PROFILE*TLE, EPHEMERIS, VISIBILITY_PASSIMAGING_REQUEST, VALIDATIONIMAGING_CANDIDATE, SCHEDULE_SLOT, schedule_runGROUND_STATION, CONTACT_SESSIONCOMMAND_*규칙: 다른 서비스의 DB를 직접 join하지 말고, API+이벤트로 복제(read model) 하세요.
POST /imaging-requestsrequest_id, status=submittedGET /imaging-requests/{id}POST /imaging-requests/{id}:cancelGET /satellites/{id}/passes?stationId=&from=&to=POST /passes:compute (캐시 갱신 트리거)POST /schedules:runsschedule_run_idGET /schedules:runs/{id}POST /schedules:runs/{id}:commit (커밋/배포)GET /satellites/{id}/schedule?from=&to=POST /commandsPOST /commands/{id}:approveGET /commands/{id}GET /products?bbox=&timeFrom=&timeTo=&cloud<=POST /deliveriesGET /deliveries/{id}Kafka 토픽 예시:
tasking.request.submittedtasking.request.validatedscheduling.candidate.generatedscheduling.schedule.committedground.session.startedground.session.completedcommand.sentcommand.acknowledgedproduct.availabledelivery.completedtelemetry.ingestedtelemetry.alert.raised이벤트 페이로드 공통 필드:
event_id, event_time, correlation_idtenant_id (멀티테넌트면 필수)entity_type, entity_idschema_version촬영은 전형적인 분산 트랜잭션이므로 SAGA를 추천합니다.
실패 시 보상 트랜잭션:
POST /imaging-requestsSCHEDULE_SLOT(planned)SCHEDULE_SLOT(committed) 이벤트Planner(요청/계획) vs Operator(승인/전송)최대화:
실무에서는 목적함수를 다음 4항으로 분해해 가중치로 운영 튜닝합니다.
(CP-SAT에서는 interval + transition constraint로 구현)
을 feature로 저장하고, 스코어링 가중치를 주기적으로 튜닝(규칙 기반→ML로 진화 가능)
원하시면 아래 중 하나를 바로 이어서 작성하겠습니다(질문은 최소화하고 바로 제시 가능합니다).
원하시는 방향을 고르지 않으셔도 됩니다. 보통은 “(3) 시퀀스 다이어그램”부터 만들면 팀 커뮤니케이션이 빨라지니, 다음 답변에서 제가 일반촬영/긴급촬영 두 개를 mermaid로 바로 제시해도 좋습니다.
좋습니다. 그러면 바로 (3) 시퀀스 다이어그램(mermaid)부터 “일반 촬영”과 “긴급 촬영(재계획)” 2종을 제시하겠습니다.
(이 다이어그램은 그대로 설계 문서/위키에 붙여 넣어도 되는 수준으로 작성했습니다.)
sequenceDiagram
autonumber
actor Customer as Customer/API Client
participant APIGW as API Gateway
participant IAM as Identity/Tenant
participant TASK as Tasking Service
participant ASSET as Satellite Asset Service
participant ORBIT as Orbit & Prediction Service
participant SCH as Scheduling & Optimization Service
participant GND as Ground Station Service
participant CMD as Commanding (TT&C) Service
participant GS as Ground Station Adapter
participant TM as Telemetry Ingest
participant ING as Payload Data Ingest
participant PROC as Processing Pipeline
participant CAT as Catalog & Search
participant DLV as Delivery & Billing
Customer->>APIGW: POST /imaging-requests (AOI, timeWindow, constraints, priority)
APIGW->>IAM: Authenticate/Authorize (tenant, quota)
IAM-->>APIGW: OK (claims, rate-limit policy)
APIGW->>TASK: Create ImagingRequest
TASK->>ASSET: Fetch satellite capabilities/profiles (optional cache)
ASSET-->>TASK: Profiles/constraints (effective-dated)
TASK->>TASK: Validate & normalize constraints (hard/soft)
TASK-->>APIGW: 202 Accepted (request_id, status=submitted)
TASK-->>ORBIT: Event tasking.request.submitted (request_id, aoi, timeWindow)
ORBIT->>ORBIT: Ensure pass cache (TLE/eph, propagate)
ORBIT-->>SCH: Event orbit.passes.ready (request_id, candidatePasses)
SCH->>SCH: Generate candidates (r,s,pass,mode) + score
SCH->>SCH: Optimize schedule (heuristic/CP-SAT) within horizon
SCH-->>TASK: Event scheduling.schedule.proposed (request_id, schedule_run_id)
TASK->>TASK: Apply policy (auto-approve or manual review)
TASK-->>SCH: Commit schedule (schedule_run_id)
SCH-->>GND: Reserve contact sessions (downlink windows)
GND->>GS: Reserve antenna/session resources
GS-->>GND: Reservation confirmed
GND-->>SCH: Reservation OK (session_id list)
SCH-->>CMD: Prepare command plan (imaging + downlink + mode transitions)
CMD->>CMD: Build command sequence (dictionary validation)
CMD-->>GND: Link commands to sessions
Note over GND,GS: At contact time (AOS~LOS)
GND->>GS: Start session (AOS)
GS->>CMD: Uplink command sequence
CMD-->>GS: Commands sent
GS-->>CMD: ACK/NAK + logs
GS->>ING: Downlink raw payload data
ING->>ING: Integrity check (checksum) + store object
ING-->>PROC: Event product.raw.ingested (uri, metadata)
PROC->>PROC: L0->L1->L2 pipeline (workflow orchestration)
PROC-->>CAT: Event product.available (product_id, footprint, level)
CAT->>CAT: Index & publish (STAC-like catalog)
CAT-->>DLV: Event product.cataloged (product_id, tenant_id)
DLV->>DLV: Delivery orchestration (API/S3/FTP/Webhook)
DLV-->>Customer: Webhook/Notification: product ready + access info
TM-->>TASK: Telemetry health events / anomalies (optional)
CMD-->>TASK: Command execution result (success/fail)
SCH-->>TASK: Execution status update (executed/failed)
Orbit passes ready는 매번 계산하지 않고 캐시(가시구간 엔터티)를 재사용.Commit 이후에는 지상국 예약/명령 준비가 이어지므로, 스케줄은 proposed → committed 상태를 명확히 분리.sequenceDiagram
autonumber
actor Operator as Operator/Planner
participant APIGW as API Gateway
participant IAM as Identity/Tenant
participant TASK as Tasking Service
participant ORBIT as Orbit & Prediction Service
participant SCH as Scheduling & Optimization Service
participant GND as Ground Station Service
participant CMD as Commanding (TT&C) Service
participant NOTI as Notification/Webhook
Operator->>APIGW: POST /imaging-requests (priority=EMERGENCY, due=ASAP)
APIGW->>IAM: AuthZ (emergency role, M-of-N if required)
IAM-->>APIGW: OK
APIGW->>TASK: Create emergency request
TASK->>TASK: Validate constraints + mark as EMERGENCY
TASK-->>Operator: 202 Accepted (request_id)
TASK-->>ORBIT: Event tasking.request.submitted (EMERGENCY)
ORBIT-->>SCH: Event orbit.passes.ready (candidatePasses)
SCH->>SCH: Load current committed schedule
SCH->>SCH: Define freeze window (now ~ now+2h) and locked slots
SCH->>SCH: Identify impacted window (e.g., now+2h ~ now+24h)
SCH->>SCH: Generate candidates for EMERGENCY request
SCH->>SCH: Re-optimize impacted window with objective:
Note right of SCH: Max(Value) - ChangePenalty - SLA penalties<br/>ChangePenalty enforces minimum perturbation
SCH-->>TASK: Event scheduling.schedule.proposed (diff set: add/move/cancel slots)
TASK->>TASK: Policy check (auto-approve emergency, notify impacted customers)
TASK-->>SCH: Commit schedule changes (schedule_run_id)
SCH-->>GND: Update reservations (add new sessions, cancel moved ones)
GND-->>CMD: Update command plan (rebuild only affected sequences)
CMD-->>TASK: Command deltas prepared
TASK-->>NOTI: Notify impacted requests (delayed/rescheduled reason codes)
NOTI-->>Operator: Emergency schedule committed + execution plan ready
가 일관되게 자동화되어야 운영이 가능합니다.
sequenceDiagram
autonumber
participant GND as Ground Station Service
participant GS as Ground Station Adapter
participant ING as Payload Data Ingest
participant SCH as Scheduling Service
participant TASK as Tasking Service
participant NOTI as Notification
GND->>GS: Downlink during session
GS-->>GND: Link degraded / downlink incomplete
GND->>GND: Mark session partially completed (link_quality low)
GND-->>ING: Event downlink.partial (expected_files, missing_chunks)
ING->>ING: Detect missing data / checksum fail
ING-->>TASK: Event product.ingest.failed (reason=INCOMPLETE_DOWNLINK)
TASK->>TASK: Create recovery action (retry downlink) + update request status
TASK-->>SCH: Event recovery.downlink.retry.requested (product/ref, deadline)
SCH->>SCH: Find next feasible contact sessions (pass cache)
SCH-->>GND: Reserve retry session
GND-->>NOTI: Notify operator/customer (delay, reason_code, new ETA)
EXECUTED_OK + DOWNLINK_PENDINGEXECUTED_OK + DOWNLINK_FAILED_RETRYING지금 다이어그램까지가 “전체 흐름의 공통 이해”를 만드는 단계라면, 다음은 팀이 바로 개발에 들어갈 수 있도록 문서화하는 단계입니다. 아래 중 하나로 이어가겠습니다(원하시면 “둘 다”도 가능합니다).
원하시는 방향을 한마디로만 주셔도 되고, 말씀 없으면 제가 (1) 상태 모델 정의서부터 이어서 계속하겠습니다.
좋습니다. “둘 다”를 (1) 상태 모델(State Machine) 정의서 + (2) 이벤트 스키마/토픽 확정(표준 규칙 포함)으로 해석하고, 바로 실무 문서 수준으로 정리합니다. (다음 단계로 DB 제약/인덱스까지 확장 가능)
여기서는 4개 핵심 오브젝트의 상태모델을 명확히 정의합니다.
각 상태는 (i) 단일 “현재 상태” + (ii) 이벤트 로그(히스토리)를 병행합니다.
SUBMITTED : 요청 접수 완료(비동기 처리 시작)VALIDATING : 요구사항 정규화/정합성 검증REJECTED : 불가(하드 제약 위반, 정책 위반, 쿼터 초과 등)VALIDATED : 검증 통과(후보 생성 대상으로 확정)PLANNING : 후보 패스/후보 촬영 윈도 생성 중PLANNED : 후보군/추천안 생성 완료(아직 슬롯 확정 전)SCHEDULING : 스케줄러가 시간축 할당 중SCHEDULED : 슬롯 확정(커밋됨)COMMANDING : 명령 생성/큐잉/승인/세션 연결 진행 중EXECUTING : 촬영 실행 구간(실제 온보드 수행 중)EXECUTED : 촬영 자체는 완료(다운링크/처리는 별개)DOWNLINK_PENDING : 데이터 하행 대기DOWNLINKED : 원시 데이터 수신 완료(ingest OK)PROCESSING : L0→L1→L2… 처리 중PRODUCT_READY : 카탈로그 등록 및 접근 가능DELIVERING : 고객 전달(푸시/다운로드/구독)COMPLETED : 전달/정산까지 완료(종결)FAILED : 최종 실패(복구 불가 또는 SLA 포기)CANCELLED : 고객/운영에 의한 취소> 실무 팁: EXECUTED 이후를 별도로 분리해야 “촬영 성공 vs 전송 실패 vs 처리 실패”를 명확히 운영할 수 있습니다.
SUBMITTED → VALIDATING : tasking.request.submittedVALIDATING → REJECTED : tasking.request.rejected (reason_code)VALIDATING → VALIDATED : tasking.request.validatedVALIDATED → PLANNING → PLANNED : orbit.passes.ready, scheduling.candidate.generatedPLANNED → SCHEDULING → SCHEDULED : scheduling.schedule.proposed, scheduling.schedule.committedSCHEDULED → COMMANDING : command.plan.preparedCOMMANDING → EXECUTING : ground.session.started 또는 command.sentEXECUTING → EXECUTED : imaging.execution.completedEXECUTED → DOWNLINK_PENDING → DOWNLINKED : downlink.*, product.raw.ingestedDOWNLINKED → PROCESSING → PRODUCT_READY : processing.started, product.availablePRODUCT_READY → DELIVERING → COMPLETED : delivery.started, delivery.completedREJECTED/FAILED에 최소 아래를 표준화하세요.
HARD_CONSTRAINT_VIOLATION (오프나딜/태양각/모드 불가 등)NO_FEASIBLE_PASS (시간창 내 패스 없음)QUOTA_EXCEEDED (테넌트/고객 제한)SATELLITE_NOT_AVAILABLE (safe mode, 유지보수)GROUND_RESOURCE_CONFLICT (지상국 혼잡)COMMAND_FAILED (ACK 실패/타임아웃)DOWNLINK_INCOMPLETE (부분 수신/체크섬 실패)PROCESSING_FAILED (파이프라인 실패)SLA_EXPIRED (납기 초과로 폐기)CREATEDRUNNINGPROPOSED (해 산출 완료)COMMITTINGCOMMITTEDABORTEDFAILED전이
RUNNING → PROPOSED : 후보/제약 만족 해 산출PROPOSED → COMMITTED : 운영 정책 승인 후 커밋PROPOSED → ABORTED : 더 나은 해 재계산/폐기FAILED : 계산 실패/데이터 불완전PLANNED : 제안된 슬롯(겹침/정합성 검증 통과 전일 수도)VALIDATED : 겹침 금지/전이 제약/자원 제약 검증 OKCOMMITTED : 운영적으로 확정(지상국/명령 준비 트리거)EXECUTINGEXECUTEDCANCELLEDSUPERSEDED : 재계획으로 대체됨(이력 보존)FAILED : 실행 실패(명령 실패 등)중요 규칙
SUPERSEDED로 마킹**LOCKED=true로 두고 재계획에서 제외DRAFT : 생성됨(검증 전)VALIDATED : 커맨드 딕셔너리/파라미터 검증 OKPENDING_APPROVAL : 승인 대기(SoD 정책)APPROVEDQUEUED : 세션/타이밍에 맞춰 큐에 적재DISPATCHING : 실제 전송 시도 중DISPATCHED : 전송 완료(ACK는 별개)ACKED : 위성 ACK 수신(또는 TM으로 확인)NACKEDTIMEOUTCANCELLEDREADYSENTACK_RECEIVEDCONFIRMED_BY_TM (권장: TM 기반 추가 확증)FAILED_RETRYINGFAILED_FINAL실무 포인트
CONFIRMED_BY_TM까지 가는 모델이 운영 신뢰도가 높습니다.PLANNED (요청에 의해 생성 예정)CAPTURED (촬영은 완료)DOWNLINK_PENDINGINGESTINGINGESTED (무결성 OK)PROCESSINGPROCESSED (L1/L2 산출)CATALOGEDPUBLISHED (고객 접근 가능)FAILED (원인 코드 포함)CREATEDAUTH_CHECKEDPACKAGING (포맷 변환/압축/타일링)DELIVERINGDELIVEREDFAILED_RETRYINGFAILED_FINALCANCELLED이 섹션은 이벤트 기반 통합을 제대로 하려면 반드시 필요한 “규약”입니다.
correlation_id를 공유correlation_id = request_id 또는 request_id 기반 ULIDschema_version 포함event_id 기반 중복처리를 방지(Exactly-once는 현실적으로 어렵고, at-least-once 전제)tenant_id가 반드시 포함되어야 함event_time(실제 발생 시간)과 produced_at(발행 시간)을 분리(지상국/위성 로그 지연 때문에 중요)도메인별로 prefix를 고정합니다.
tasking.request.submittedtasking.request.validatedtasking.request.rejectedtasking.request.cancelledtasking.request.status.changedorbit.tle.ingestedorbit.passes.readyorbit.passes.invalidated (TLE 갱신 등으로 캐시 무효화)scheduling.candidate.generatedscheduling.schedule.proposedscheduling.schedule.committedscheduling.schedule.supersededground.session.reservedground.session.startedground.session.completedground.session.failedcommand.plan.preparedcommand.request.approvedcommand.sentcommand.acknowledgedcommand.failedproduct.raw.ingestedproduct.ingest.failedprocessing.startedprocessing.failedproduct.availablecatalog.product.indexeddelivery.starteddelivery.completeddelivery.failedbilling.usage.recorded모든 이벤트는 아래 Envelope를 공통으로 씁니다.
{
"event_id": "01J...ULID",
"event_type": "scheduling.schedule.committed",
"schema_version": 1,
"tenant_id": "tnt_123",
"correlation_id": "req_456",
"entity": {
"type": "ScheduleRun",
"id": "schrun_789"
},
"event_time": "2026-03-13T10:15:00Z",
"produced_at": "2026-03-13T10:15:01Z",
"producer": {
"service": "scheduling-service",
"instance_id": "sched-7f9d"
},
"payload": {}
}
{
"request_id": "req_456",
"priority": "NORMAL|HIGH|EMERGENCY",
"time_window": { "start": "...", "end": "..." },
"aoi": {
"type": "Polygon",
"coordinates": [[[...]]]
},
"constraints": {
"max_off_nadir_deg": 25,
"max_cloud_pct": 20,
"product_level": "L1|L2",
"latency_minutes": 180
}
}
{
"request_id": "req_456",
"candidates": [
{
"satellite_id": "sat_1",
"pass_id": "pass_abc",
"aos_time": "...",
"los_time": "...",
"max_elevation_deg": 62.1
}
],
"orbit_source": {
"type": "TLE",
"epoch_time": "..."
}
}
{
"schedule_run_id": "schrun_789",
"horizon": { "from": "...", "to": "..." },
"freeze_window": { "from": "...", "to": "..." },
"slots": [
{
"slot_id": "slot_001",
"satellite_id": "sat_1",
"type": "IMAGING|DOWNLINK|MANEUVER",
"start_time": "...",
"end_time": "...",
"linked_request_id": "req_456",
"score": 0.873,
"state": "PLANNED|COMMITTED"
}
],
"diff": {
"added": ["slot_001"],
"moved": ["slot_010"],
"cancelled": ["slot_003"],
"superseded": ["slot_007"]
}
}
{
"cmd_req_id": "cmd_123",
"satellite_id": "sat_1",
"session_id": "sess_55",
"command_type": "PAYLOAD_START|PAYLOAD_STOP|MODE_CHANGE|DOWNLINK_START",
"sent_at": "...",
"ack_at": "...",
"result": "ACK|NACK|TIMEOUT",
"reason_code": "..."
}
{
"product_id": "prd_999",
"request_id": "req_456",
"satellite_id": "sat_1",
"sensing_time": "...",
"level": "L1",
"footprint": { "type": "Polygon", "coordinates": [[[...]]] },
"cloud_cover_est": 12.3,
"uri": "s3://bucket/path/file.tif",
"checksum": "sha256:..."
}
원하시면 이어서 바로 작성하겠습니다(역시 질문 최소화하고 베이스라인으로 진행).
원하시면 다음 답변에서 제가 Transition Table + Event Catalog를 바로 만들어서 계속 이어가겠습니다.
좋습니다. 바로 (1) 상태 전이 표(Transition Table)와 (2) Event Catalog(토픽별 Producer/Consumer/SLA/재시도/DLQ)를 “문서 그대로 붙여넣기 가능한 수준”으로 작성합니다.
(표가 길어질 수 있어 핵심 객체 4종을 분리해 제시하고, 운영에서 반드시 필요한 예외/복구 전이까지 포함합니다.)
표기 규칙
→ 다음 상태* 는 “어디서든 들어올 수 있음(전역 전이)”| Current | E(Event) | G(조건) | Next | A(후처리) |
|---|---|---|---|---|
| SUBMITTED | tasking.request.submitted | request persisted | VALIDATING | validate/normalize constraints |
| VALIDATING | tasking.request.validated | all hard checks pass | VALIDATED | emit validated event |
| VALIDATING | tasking.request.rejected | any hard check fail | REJECTED | set reason_code, notify |
| VALIDATED | orbit.passes.ready | passes computed | PLANNING | start candidate gen |
| PLANNING | scheduling.candidate.generated | candidates exist | PLANNED | store candidates + scores |
| PLANNED | scheduling.schedule.proposed | schedule_run created | SCHEDULING | policy check start |
| SCHEDULING | scheduling.schedule.committed | approved/auto-approved | SCHEDULED | freeze slots, trigger GS reserve |
| SCHEDULED | command.plan.prepared | command plan built | COMMANDING | create command requests |
| COMMANDING | ground.session.started | AOS reached | EXECUTING | dispatch queued cmds |
| EXECUTING | imaging.execution.completed | payload capture done | EXECUTED | create/advance DataProduct |
| EXECUTED | downlink.required | product not yet downlinked | DOWNLINK_PENDING | enqueue downlink plan |
| DOWNLINK_PENDING | product.raw.ingested | checksum OK | DOWNLINKED | trigger processing |
| DOWNLINKED | processing.started | pipeline accepted | PROCESSING | start workflow |
| PROCESSING | product.available | processed OK | PRODUCT_READY | catalog index |
| PRODUCT_READY | delivery.started | delivery order exists | DELIVERING | package/push |
| DELIVERING | delivery.completed | delivered | COMPLETED | billing usage record |
| Current | E(Event) | G(조건) | Next | A(후처리) |
|---|---|---|---|---|
| \* | tasking.request.cancelled | cancel policy allows | CANCELLED | supersede slots/commands if any |
| PLANNED/ SCHEDULING | scheduling.schedule.failed | solver error | FAILED | notify operator, keep request for retry |
| SCHEDULED | satellite.unavailable | safe mode / maintenance | FAILED or PLANNED | create recovery option (replan) |
| COMMANDING | command.failed | NACK/TIMEOUT and no retry | FAILED | reason_code=COMMAND_FAILED |
| COMMANDING | command.failed | retryable and retries left | COMMANDING | retry with backoff |
| EXECUTED / DOWNLINK_PENDING | product.ingest.failed | incomplete downlink | DOWNLINK_PENDING | emit recovery.downlink.retry.requested |
| PROCESSING | processing.failed | retryable | PROCESSING | retry pipeline (limit N) |
| PROCESSING | processing.failed | non-retryable | FAILED | reason_code=PROCESSING_FAILED |
| \* | sla.expired | due time exceeded | FAILED | reason_code=SLA_EXPIRED |
> 운영 팁: FAILED를 “종결”로만 쓰지 말고, 복구 가능한 실패는 상태+이벤트로 재시도하세요. 다만 SLA 만료는 종결이 맞습니다.
| Current | E | G | Next | A |
|---|---|---|---|---|
| CREATED | scheduling.run.started | input ready | RUNNING | lock snapshot of constraints |
| RUNNING | scheduling.run.proposed | feasible solution found | PROPOSED | persist slots (PLANNED) |
| RUNNING | scheduling.run.failed | solver failure | FAILED | record diagnostics |
| PROPOSED | scheduling.run.commit.requested | approved | COMMITTING | validate overlaps + freeze window |
| COMMITTING | scheduling.schedule.committed | validation OK | COMMITTED | mark slots COMMITTED, emit diff |
| COMMITTING | scheduling.run.aborted | validation fail | ABORTED | rollback planned slots |
| Current | E | G | Next | A |
|---|---|---|---|---|
| PLANNED | scheduling.slot.validated | no overlap + transitions OK | VALIDATED | attach resource refs |
| VALIDATED | scheduling.schedule.committed | run committed | COMMITTED | trigger GS reserve + command plan |
| COMMITTED | ground.session.started | time reached | EXECUTING | mark executing |
| EXECUTING | imaging.execution.completed | slot_type=IMAGING | EXECUTED | create product artifact |
| EXECUTING | ground.session.completed | slot_type=DOWNLINK | EXECUTED | downlink stats |
| COMMITTED/VALIDATED/PLANNED | scheduling.slot.cancelled | cancel policy allows | CANCELLED | free resources |
| COMMITTED | scheduling.slot.superseded | replan committed | SUPERSEDED | keep history immutably |
| EXECUTING | execution.failed | anomaly | FAILED | emit reason_code |
DB 레벨 권장 제약(다음 단계에서 상세화 가능)
(start,end)는 겹침 금지(Exclusion)COMMITTED 슬롯은 freeze window 내 변경 제한(정책+DB락/버전)| Current | E | G | Next | A |
|---|---|---|---|---|
| DRAFT | command.validated | dictionary OK | VALIDATED | freeze payload |
| VALIDATED | command.approval.requested | high-risk? | PENDING_APPROVAL | require M-of-N |
| PENDING_APPROVAL | command.approved | approvals satisfied | APPROVED | enqueue for session |
| APPROVED | command.queued | session planned | QUEUED | set dispatch time |
| QUEUED | ground.session.started | AOS | DISPATCHING | send |
| DISPATCHING | command.sent | sent | DISPATCHED | await ack |
| DISPATCHED | command.acknowledged | ACK | ACKED | optionally wait TM confirm |
| DISPATCHED | command.failed | NACK/TIMEOUT | NACKED/TIMEOUT | retry or fail |
| \* | command.cancelled | before dispatch | CANCELLED | audit log |
| Current | E | G | Next | A |
|---|---|---|---|---|
| READY | command.sent | sent | SENT | store raw logs |
| SENT | command.acknowledged | ACK | ACK_RECEIVED | emit event |
| ACK_RECEIVED | telemetry.confirmed | TM confirms effect | CONFIRMED_BY_TM | mark final success |
| SENT | command.failed | retryable | FAILED_RETRYING | schedule retry |
| SENT | command.failed | non-retryable | FAILED_FINAL | reason_code |
| Current | E | G | Next | A |
|---|---|---|---|---|
| PLANNED | imaging.execution.completed | captured | CAPTURED | set sensing_time |
| CAPTURED | downlink.required | not received | DOWNLINK_PENDING | create downlink request |
| DOWNLINK_PENDING | product.raw.ingested | checksum OK | INGESTED | persist uri+checksum |
| INGESTED | processing.started | accepted | PROCESSING | start workflow |
| PROCESSING | product.available | done | PROCESSED | derive metadata |
| PROCESSED | catalog.product.indexed | indexed | CATALOGED | publish |
| CATALOGED | product.published | access granted | PUBLISHED | ready for delivery |
| \* | product.ingest.failed | retryable | DOWNLINK_PENDING | recovery request |
| PROCESSING | processing.failed | non-retryable | FAILED | reason_code |
| Current | E | G | Next | A |
|---|---|---|---|---|
| CREATED | delivery.auth.checked | permitted | AUTH_CHECKED | check entitlements |
| AUTH_CHECKED | delivery.started | packaging needed | PACKAGING | format/tiling |
| PACKAGING | delivery.started | ready | DELIVERING | push |
| DELIVERING | delivery.completed | ok | DELIVERED | billing record |
| DELIVERING | delivery.failed | retryable | FAILED_RETRYING | retry policy |
| FAILED_RETRYING | delivery.retry | retries left | DELIVERING | backoff |
| FAILED_RETRYING | delivery.failed | retries exhausted | FAILED_FINAL | notify |
표기 규칙
dlq.tasking.request.submitted (schema error/validation fail 시)dlq.orbit.passes.ready (좌표/시간 범위 오류 등)dlq.scheduling.schedule.committed + operator alert (치명)partition_key = request_idpartition_key = session_idevent_id + entity.id 조합으로 소비자 중복 방지여기까지가 “상태/이벤트 계약”을 확정한 단계입니다. 다음에 바로 효과가 나는 것은 보통 아래 2개입니다.
원하시면 제가 다음 답변에서 (3) DB 설계서부터 이어서 계속 작성하겠습니다.
좋습니다. 그럼 바로 (3) DB 제약/인덱스/파티셔닝 설계서를 “실제 구축 가능한 수준”으로 정리하겠습니다. (PostgreSQL + PostGIS + TimescaleDB 조합을 기준으로 작성하되, 다른 스택이어도 원칙은 동일합니다.)
아래는 실무에서 장애/데이터 오염을 막는 핵심입니다.
slot_id (PK)satellite_id (FK)start_time, end_timeslot_typestate (PLANNED/COMMITTED/...)version (낙관적 락)schedule_run_idlocked (freeze window용 boolean)CHECK (end_time > start_time)CHECK (state IN (...))PostgreSQL + GIST를 이용해 “DB가 겹침을 허용하지 않게” 강제합니다.
tsrange(start_time, end_time, '[)') 를 사용권장 논리:
PLANNED, VALIDATED, COMMITTED, EXECUTING> 구현 팁: 조건부 exclusion은 구현 방식이 몇 가지라, 간단히는 “활성 슬롯 테이블”과 “이력 슬롯 테이블”을 분리(아카이빙)하는 것도 좋은 선택입니다.
(satellite_id, start_time)(state, start_time)(schedule_run_id)(satellite_id, tsrange)에 GIST 인덱스version 비교 업데이트UPDATE ... WHERE slot_id=? AND version=? 성공 여부로 충돌 감지request_id (PK)tenant_idaoi_geom geometry(Polygon,4326)time_window_start/endpriority, sla_due_timestatus, reason_codeCHECK (time_window_end > time_window_start)CHECK (priority BETWEEN 1 AND N) (혹은 ENUM)CHECK (max_cloud_pct BETWEEN 0 AND 100) 등GIST(aoi_geom) : AOI 교차/포함 검색(tenant_id, created_at desc)(status, priority, sla_due_time)(time_window_start, time_window_end) 또는 tsrange 인덱스> 운영 팁: “기간이 넓은 요청”이 많으면 time-window 검색 비용이 커집니다. tsrange 기반으로 통일하는 게 스케줄링에 유리합니다.
pass_id (PK)satellite_id, station_idaos_time, los_timemax_elevation, predicted_atorbit_source_refCHECK (los_time > aos_time)CHECK (max_elevation BETWEEN 0 AND 90)(satellite_id, aos_time)(station_id, aos_time)(predicted_at desc) : 최신 예측 우선(satellite_id, requested_at desc)(status, priority, requested_at)(correlation_id) : 요청 단위 트레이스(session_id, sent_at)(cmd_req_id)(result, sent_at desc)command_type는 ENUMpayload_json 스키마 검증은 앱 레벨 + (가능하면) JSON schema validation 파이프라인product_id (PK)tenant_id, request_id, satellite_idsensing_timefootprint_geom geometry(Polygon,4326)level, format, processing_statusuri, checksumGIST(footprint_geom)(tenant_id, sensing_time desc)(satellite_id, sensing_time desc)(processing_status, updated_at) : 처리 큐/모니터링(level, format) : 필터링timesatellite_id (Timescale의 space partition)(satellite_id, time desc)(parameter_code, time desc) (자주 보는 파라미터만)> 운영 팁: 텔레메트리는 “원시 유지”에 집착하면 비용 폭증합니다. 롤업 테이블을 기본 설계로 포함하세요.
패스는 많이 쌓입니다(위성 수×지상국 수×일).
권장 파티션:
aos_time (또는 predicted_at)event_id, event_time, type, entity_id, uri만 두는 방식이 비용 효율적tenant_id(tenant_id, time) 혹은 (tenant_id, status) 복합 인덱스대부분 위성 플랫폼은 옵션1로 시작하고, 특정 대형 고객만 옵션2/전용 DB로 분기합니다.
(satellite_id, command_type, dispatch_window, correlation_id) 유니크(정책에 따라)checksum NOT NULL (INGESTED 이상 상태에서)DELIVERY는 PRODUCT_READY 이전이면 생성 불가(앱/워크플로에서 강제)DB 설계서 다음으로 팀이 바로 개발/QA 들어가려면 Contract Map이 제일 효율이 좋습니다.
원하시면 다음 답변에서 제가 Contract Map(표 + 시퀀스)를 만들어서 계속 이어가겠습니다.
좋습니다. 아래는 요청하신 (4) API + 이벤트 상호참조(Contract Map) 입니다.
목표는 “개발/QA/운영이 같은 그림을 보게” 만드는 것이고, API 호출 ↔ 이벤트 발행/소비 ↔ DB 변경 ↔ 상태전이를 한 장의 계약으로 고정합니다.
API
POST /imaging-requests (Tasking)Tasking: DB Write
IMAGING_REQUEST(status=SUBMITTED)REQUEST_EVENT_LOG(append-only, optional)Tasking: Emit
tasking.request.submittedState
SUBMITTED → VALIDATING (내부)VALIDATED 또는 REJECTEDIdempotency
Idempotency-Key(HTTP header) + (tenant_id, client_request_id) 유니크 권장request_id 반환(201/202가 아닌 200 + existing resource)API
POST /imaging-requests/{id}:validate (옵션)Tasking: DB Write
IMAGING_REQUEST status 업데이트REASON_CODE 기록Tasking: Emit
tasking.request.validated 또는 tasking.request.rejectedState
VALIDATING → VALIDATED or REJECTEDIdempotency
request_id 단위 멱등(이미 VALIDATED면 no-op)Consume
tasking.request.submitted 또는 tasking.request.validatedOrbit: DB Write
TLE/EPHEMERIS_STATE_VECTOR(이미 있으면 reuse)VISIBILITY_PASS(request horizon 범위 캐시/갱신)REQUEST_PASS_MAP(request_id와 pass_id 매핑 캐시)Orbit: Emit
orbit.passes.readyIdempotency
(satellite_id, station_id, aos_time, los_time, orbit_epoch) 또는 pass_cache_keyConsume
orbit.passes.readyScheduling: DB Write
IMAGING_CANDIDATE (feasible_flag, score)SCHEDULE_RUN(CREATED/RUNNING)Scheduling: Emit
scheduling.candidate.generatedtasking.request.status.changed(PLANNED로)State
VALIDATED/PLANNING → PLANNEDIdempotency
(request_id, pass_id, satellite_id, mode_code) 유니크API
POST /schedules:runs (Scheduling)POST /schedules:runs/{id}:commit (Scheduling)(커밋은 정책상 Tasking이 호출할 수도 있음)
Scheduling: DB Write
SCHEDULE_RUN RUNNING→PROPOSED→COMMITTEDSCHEDULE_SLOT(PLANNED→VALIDATED→COMMITTED)SUPERSEDED 처리(삭제 금지)Scheduling: Emit
scheduling.schedule.proposedscheduling.schedule.committedState
PLANNED/SCHEDULING → SCHEDULEDVALIDATED → COMMITTEDIdempotency
schedule_run_id 멱등(satellite_id, tsrange) 겹침 금지(Exclusion)로 DB가 최종 방어Consume
scheduling.schedule.committedscheduling.schedule.committedGround: API/Adapter Call
ReserveSession(station, aos/los, band, antenna) to GS AdapterGround: DB Write
CONTACT_SESSION status=RESERVEDSESSION_RESOURCE_LOCK(안테나/자원 캘린더)Ground: Emit
ground.session.reservedCommanding: DB Write
COMMAND_REQUEST(VALIDATED/PENDING_APPROVAL/QUEUED)COMMAND_SEQUENCE(세션별 시퀀스, optional)Commanding: Emit
command.plan.preparedState
SCHEDULED → COMMANDINGIdempotency
(station_id, antenna_id, tsrange) 겹침 금지(slot_id, command_type, sequence_no) 유니크Consume
Ground: Emit
ground.session.startedCommanding: Emit
command.sentcommand.acknowledged or command.failedGround: Emit
ground.session.completed (link stats)DB Write
CONTACT_SESSION(STARTED/COMPLETED/FAILED)COMMAND_EXECUTION(SENT/ACK/FAIL)SCHEDULE_SLOT(EXECUTING/EXECUTED)State
COMMITTED → EXECUTING → EXECUTEDCOMMANDING → EXECUTING → EXECUTEDIdempotency
session_id partition key, 순서 보장event_id 기반 dedupeConsume
ground.session.completed(or adapter raw downlink event)Ingest: DB Write
DATA_PRODUCT(CAPTURED/DOWNLINK_PENDING→INGESTED)PRODUCT_FILE_MANIFEST(파일/청크/체크섬)Ingest: Emit
product.raw.ingested 또는 product.ingest.failedProcessing consumes
product.raw.ingestedProcessing: DB Write
PROCESS_JOB(RUNNING/SUCCEEDED/FAILED)DATA_PRODUCT(PROCESSING→PROCESSED)Processing: Emit
product.available / processing.failedState
DOWNLINK_PENDING → DOWNLINKED → PROCESSING → PRODUCT_READYIdempotency
(product_id, checksum) 또는 (uri, checksum) 유니크(product_id, pipeline_version) 유니크Catalog consumes
product.availableCatalog: DB Write
CATALOG_ITEM(STAC-like), 공간 인덱스 갱신DATA_PRODUCT(CATALOGED/PUBLISHED)Catalog: Emit
catalog.product.indexedDelivery consumes
catalog.product.indexed (또는 product.available 직접)Delivery: API
POST /deliveries (internal or customer action)GET /deliveries/{id}Delivery: DB Write
DELIVERY_ORDER(DELIVERING/DELIVERED/FAILED)BILLING_RECORD or USAGE_EVENTDelivery: Emit
delivery.completed / delivery.failedbilling.usage.recordedState
DELIVERING → COMPLETEDIdempotency
(tenant_id, product_id, method, destination) 유니크POST /imaging-requests with priority=EMERGENCYPOST /schedules:runs with {freeze_window, objective=min_perturbation}scheduling.schedule.proposed payload에 diff(added/moved/cancelled/superseded) 필수tasking.request.status.changed로 영향받은 기존 요청들의 status/eta 변경 통지locked=true 슬롯은 업데이트 금지(정책+DB락/버전)SUPERSEDED로 보존tasking.request.submitted → orbit.passes.ready → scheduling.schedule.committed → product.available → delivery.completedtasking.request.rejected(reason=HARD_CONSTRAINT_VIOLATION), downstream 이벤트 없음product.ingest.failed(reason=DOWNLINK_INCOMPLETE)recovery.downlink.retry.requestedproduct.raw.ingested 성공(여기서 흔히 팀 간 갈등이 나서 미리 고정해두는 항목입니다.)
PROCESSED인지 CATALOGED/PUBLISHED까지인지