mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-30 18:46:00 +00:00
56655d53b4
* feat(admin): register AirTrail as an integration addon
Off by default; toggle lives in Admin -> Addons with a Plane icon. The
per-user connection (URL + API key) follows in integration settings.
* feat(integrations): add per-user AirTrail connection
Settings -> Integrations gains an AirTrail section: instance URL + Bearer
API key (encrypted at rest via apiKeyCrypto), a self-signed-TLS opt-in and
a test-connection check. Served by a small Nest controller under
/api/integrations/airtrail, gated on the airtrail addon and SSRF-guarded.
The key is per-user, so it only ever returns that user's own flights.
* feat(transport): import flights from AirTrail
Adds an AirTrail Import button next to Manual Transport that lists the
user's AirTrail flights and highlights the ones inside the trip dates.
Selected flights become reservations linked to their AirTrail origin
(external_* columns), deduped against flights already in the trip, then
broadcast to every member. The mapping resolves airports, airport-local
times and flight metadata; the linkage is what the two-way sync rides on.
* feat(transport): badge AirTrail-linked flights as synced
Linked reservations show an 'AirTrail synced' badge, or 'no longer
synced' once the flight is gone from AirTrail.
* feat(transport): keep TREK and AirTrail flights in sync both ways
A scheduled poll reconciles each connected owner's flights: field edits
(detected by snapshot hash, since AirTrail has no updated_at) flow into
the linked reservation and broadcast live; a flight deleted in AirTrail
keeps the TREK row but stops syncing. Editing a linked flight in TREK
pushes back to AirTrail under the importer's credentials, preserving the
existing seat manifest; if the owner disconnected the link detaches so the
poll can't revert the local edit. Deleting in TREK never touches AirTrail.
* i18n(airtrail): add AirTrail strings across all locales
* test(airtrail): cover flight mapping, timezones and snapshot hashing
* fix(airtrail): reduce airline/aircraft objects to codes
The flight list/get response returns airline and aircraft as joined
objects ({icao, iata, name, ...}), not bare codes. Mapping them straight
through produced '[object Object]' titles and stored objects in metadata,
which crashed reservation rendering. Extract the ICAO/IATA code instead,
and title flights by their flight number.
* fix(airtrail): clear error on non-JSON responses, tolerate /api in URL
A misconfigured instance URL made AirTrail serve its SPA/login HTML, and
the raw JSON.parse failure surfaced as 'Unexpected token <'. Surface an
actionable message instead, and strip a pasted trailing /api so the base
URL still resolves.
* feat(transport): sync AirTrail edits on trip open, not just on the poll
Add a per-user on-demand sync (POST /integrations/airtrail/sync) triggered
when a connected user opens a trip, so AirTrail-side edits appear right away
instead of waiting up to a full poll cycle. Lower the background poll from 15
to 5 minutes as a safety net.
* fix(transport): refresh imported AirTrail flights without a reload
loadTrip doesn't fetch reservations, so a freshly imported flight only
appeared after a full page reload — use loadReservations instead. Also show
flight dates in the user's locale format (e.g. 13.06.2026) rather than the
raw ISO string.
* style(settings): align AirTrail connection with the photo-provider layout
Match the Immich section: stacked URL/key fields, a ToggleSwitch for
self-signed TLS, and a Save / Test-connection row with a status badge.
* feat(transport): add a seat field when editing flights
The transport editor only offered a seat field for trains; flights had
none even though imports store metadata.seat. Show and persist a seat for
flights too.
* style(transport): match the AirTrail button height to Manual Transport
* feat(transport): put the flight seat next to flight number and sync it to AirTrail
Move the seat from a standalone row to the per-leg flight details (beside
the flight number), stored per leg in metadata.legs[].seat with the first
leg mirrored to metadata.seat. On push, set the seat number on the user's
own AirTrail seat (the one with a userId), leaving co-passengers untouched;
import/poll read that same seat back.
* refactor(planner): move the AirTrail trip-open sync into useTripPlanner
Page containers must not own state/effects (lint:pages). Same logic,
relocated from the page into its data hook.
* test(db): pin the region-reconciliation test to its schema version
The test re-ran 'the last migration' assuming the reconciliation is last;
it no longer is once later migrations are appended. Pin to version 135 and
re-run from there (the appended migrations are idempotent).
161 lines
8.7 KiB
TypeScript
161 lines
8.7 KiB
TypeScript
import type { TranslationStrings } from '../types';
|
|
|
|
const reservations: TranslationStrings = {
|
|
'reservations.title': '예약',
|
|
'reservations.empty': '아직 예약이 없습니다',
|
|
'reservations.emptyHint': '항공, 호텔 등의 예약을 추가하세요',
|
|
'reservations.add': '예약 추가',
|
|
'reservations.addManual': '직접 예약',
|
|
'reservations.placeHint':
|
|
'팁: 예약은 일별 계획과 연결하기 위해 장소에서 직접 만드는 것이 가장 좋습니다.',
|
|
'reservations.confirmed': '확정됨',
|
|
'reservations.pending': '대기 중',
|
|
'reservations.summary': '{confirmed}개 확정, {pending}개 대기 중',
|
|
'reservations.fromPlan': '계획에서',
|
|
'reservations.showFiles': '파일 보기',
|
|
'reservations.editTitle': '예약 편집',
|
|
'reservations.status': '상태',
|
|
'reservations.datetime': '날짜 및 시간',
|
|
'reservations.startTime': '시작 시간',
|
|
'reservations.endTime': '종료 시간',
|
|
'reservations.date': '날짜',
|
|
'reservations.time': '시간',
|
|
'reservations.timeAlt': '시간 (대안, 예: 19:30)',
|
|
'reservations.notes': '메모',
|
|
'reservations.notesPlaceholder': '추가 메모...',
|
|
'reservations.meta.airline': '항공사',
|
|
'reservations.meta.flightNumber': '항공편 번호',
|
|
'reservations.meta.from': '출발',
|
|
'reservations.meta.to': '도착',
|
|
'reservations.layover.route': '경로',
|
|
'reservations.layover.stop': '경유',
|
|
'reservations.layover.addStop': '경유지 추가',
|
|
'reservations.layover.connection': '연결편',
|
|
'reservations.layover.layover': '경유 대기',
|
|
'reservations.needsReview': '검토 필요',
|
|
'reservations.needsReviewHint':
|
|
'공항이 자동으로 매칭되지 않았습니다 — 위치를 확인해 주세요.',
|
|
'reservations.searchLocation': '역, 항구, 주소 검색…',
|
|
'reservations.meta.trainNumber': '열차 번호',
|
|
'reservations.meta.platform': '플랫폼',
|
|
'reservations.meta.seat': '좌석',
|
|
'reservations.meta.checkIn': '체크인',
|
|
'reservations.meta.checkInUntil': '체크인 마감',
|
|
'reservations.meta.checkOut': '체크아웃',
|
|
'reservations.meta.linkAccommodation': '숙박',
|
|
'reservations.meta.pickAccommodation': '숙박 연결',
|
|
'reservations.meta.noAccommodation': '없음',
|
|
'reservations.meta.hotelPlace': '숙박',
|
|
'reservations.meta.pickHotel': '숙박 선택',
|
|
'reservations.meta.fromDay': '부터',
|
|
'reservations.meta.toDay': '까지',
|
|
'reservations.meta.selectDay': '날 선택',
|
|
'reservations.type.flight': '항공',
|
|
'reservations.type.hotel': '숙박',
|
|
'reservations.type.restaurant': '레스토랑',
|
|
'reservations.type.train': '기차',
|
|
'reservations.type.car': '차량',
|
|
'reservations.type.cruise': '크루즈',
|
|
'reservations.type.event': '이벤트',
|
|
'reservations.type.tour': '투어',
|
|
'reservations.type.other': '기타',
|
|
'reservations.type.bus': '버스',
|
|
'reservations.type.ferry': '페리',
|
|
'reservations.type.bicycle': '자전거',
|
|
'reservations.type.taxi': '택시',
|
|
'reservations.type.transport_other': '기타',
|
|
'reservations.confirm.delete': '예약 "{name}"을(를) 삭제할까요?',
|
|
'reservations.confirm.deleteTitle': '예약을 삭제할까요?',
|
|
'reservations.confirm.deleteBody': '"{name}"이(가) 영구 삭제됩니다.',
|
|
'reservations.toast.updated': '예약이 업데이트되었습니다',
|
|
'reservations.toast.removed': '예약이 삭제되었습니다',
|
|
'reservations.toast.fileUploaded': '파일이 업로드되었습니다',
|
|
'reservations.toast.uploadError': '업로드 실패',
|
|
'reservations.newTitle': '새 예약',
|
|
'reservations.bookingType': '예약 유형',
|
|
'reservations.titleLabel': '제목',
|
|
'reservations.titlePlaceholder': '예: 대한항공 KE123, 호텔 신라, ...',
|
|
'reservations.locationAddress': '위치 / 주소',
|
|
'reservations.locationPlaceholder': '주소, 공항, 호텔...',
|
|
'reservations.confirmationCode': '예약 코드',
|
|
'reservations.confirmationPlaceholder': '예: ABC12345',
|
|
'reservations.day': '날',
|
|
'reservations.noDay': '날 없음',
|
|
'reservations.place': '장소',
|
|
'reservations.noPlace': '장소 없음',
|
|
'reservations.pendingSave': '저장될 예정…',
|
|
'reservations.uploading': '업로드 중...',
|
|
'reservations.attachFile': '파일 첨부',
|
|
'reservations.linkExisting': '기존 파일 연결',
|
|
'reservations.toast.saveError': '저장 실패',
|
|
'reservations.toast.updateError': '업데이트 실패',
|
|
'reservations.toast.deleteError': '삭제 실패',
|
|
'reservations.confirm.remove': '"{name}"의 예약을 제거할까요?',
|
|
'reservations.linkAssignment': '날 배정에 연결',
|
|
'reservations.pickAssignment': '계획에서 배정을 선택하세요...',
|
|
'reservations.noAssignment': '연결 없음 (독립)',
|
|
'reservations.price': '가격',
|
|
'reservations.budgetCategory': '예산 카테고리',
|
|
'reservations.budgetCategoryPlaceholder': '예: 교통, 숙박',
|
|
'reservations.budgetCategoryAuto': '자동 (예약 유형에서)',
|
|
'reservations.budgetHint': '저장 시 예산 항목이 자동으로 생성됩니다.',
|
|
'reservations.departureDate': '출발',
|
|
'reservations.arrivalDate': '도착',
|
|
'reservations.departureTime': '출발 시간',
|
|
'reservations.arrivalTime': '도착 시간',
|
|
'reservations.pickupDate': '픽업',
|
|
'reservations.returnDate': '반납',
|
|
'reservations.pickupTime': '픽업 시간',
|
|
'reservations.returnTime': '반납 시간',
|
|
'reservations.endDate': '종료 날짜',
|
|
'reservations.meta.departureTimezone': '출발 시간대',
|
|
'reservations.meta.arrivalTimezone': '도착 시간대',
|
|
'reservations.span.departure': '출발',
|
|
'reservations.span.arrival': '도착',
|
|
'reservations.span.inTransit': '이동 중',
|
|
'reservations.span.pickup': '픽업',
|
|
'reservations.span.return': '반납',
|
|
'reservations.span.active': '활성',
|
|
'reservations.span.start': '시작',
|
|
'reservations.span.end': '종료',
|
|
'reservations.span.ongoing': '진행 중',
|
|
'reservations.validation.endBeforeStart':
|
|
'종료 날짜/시간은 시작 날짜/시간 이후여야 합니다',
|
|
'reservations.addBooking': '예약 추가',
|
|
'reservations.import.title': '예약 확인서 가져오기',
|
|
'reservations.import.cta': '파일에서 가져오기',
|
|
'reservations.import.dropHere': '예약 확인 파일을 여기에 끌어다 놓거나 클릭하여 선택',
|
|
'reservations.import.dropActive': '가져올 파일을 여기에 놓으세요',
|
|
'reservations.import.acceptedFormats': '허용 형식: EML, PDF, PKPass, HTML, TXT (파일당 최대 10 MB, 최대 5개)',
|
|
'reservations.import.parsing': '파일 분석 중…',
|
|
'reservations.import.previewHeading': '{count}개 예약 발견',
|
|
'reservations.import.previewEmpty': '업로드된 파일에서 예약을 추출할 수 없었습니다.',
|
|
'reservations.import.removeItem': '제거',
|
|
'reservations.import.confirm': '{count}개 예약 가져오기',
|
|
'reservations.import.back': '뒤로',
|
|
'reservations.import.success': '{count}개 예약을 가져왔습니다',
|
|
'reservations.import.partialFailure': '{created}개 가져옴, {failed}개 실패',
|
|
'reservations.import.error': '분석 실패. 파일이 유효한 예약 확인서인지 확인하세요.',
|
|
'reservations.import.unavailable': '이 서버에서는 예약 가져오기를 사용할 수 없습니다.',
|
|
'reservations.import.unsupportedFormat': '지원하지 않는 파일 형식입니다. EML, PDF, PKPass, HTML 또는 TXT를 사용하세요.',
|
|
'reservations.import.fileTooLarge': '파일 "{name}"이(가) 10 MB 제한을 초과합니다.',
|
|
'reservations.airtrail.title': 'AirTrail에서 가져오기',
|
|
'reservations.airtrail.cta': 'AirTrail',
|
|
'reservations.airtrail.synced': 'AirTrail',
|
|
'reservations.airtrail.syncedHint': 'AirTrail에서 동기화됨 — 수정 사항이 양방향으로 동기화됩니다.',
|
|
'reservations.airtrail.notSynced': '동기화되지 않음',
|
|
'reservations.airtrail.notSyncedHint': '이 항공편은 AirTrail에서 삭제되어 더 이상 동기화되지 않습니다.',
|
|
'reservations.airtrail.loadError': 'AirTrail 항공편을 불러올 수 없습니다.',
|
|
'reservations.airtrail.imported': '{count}개 항공편을 가져왔습니다',
|
|
'reservations.airtrail.skippedDuplicate': '{count}개는 이미 이 여행에 있어 건너뛰었습니다',
|
|
'reservations.airtrail.nothingImported': '가져올 항목이 없습니다.',
|
|
'reservations.airtrail.importError': '가져오기에 실패했습니다. 다시 시도하세요.',
|
|
'reservations.airtrail.undo': 'AirTrail에서 가져오기',
|
|
'reservations.airtrail.alreadyImported': '가져옴',
|
|
'reservations.airtrail.duringTrip': '이 여행 기간',
|
|
'reservations.airtrail.otherFlights': '기타 항공편',
|
|
'reservations.airtrail.empty': 'AirTrail 계정에서 항공편을 찾을 수 없습니다.',
|
|
'reservations.airtrail.importCta': '{count}개 가져오기',
|
|
};
|
|
export default reservations;
|