17 KiB
hop. 구현 설계와 검증 계획
기준: SPEC.md v3.0. 초기 분석 시 저장소는 명세만 포함했다. 이후 진행과 실제 검증 결과는 VERIFICATION.md에 기록한다.
사용자 추가 결정(2026-10-08): 배포 포트·접속 정보는 필요할 때 확인하며 샘플 포트를 고정하지 않는다. 익명 자료의 계정 저장은 기존 익명 접근권을 제거하는 이동이다. 이메일 발송은 기존 Resend 연동(hello@noname.company)을 사용한다. 아래 초기 SMTP 계획보다 이 사용자 지침을 우선한다. 신규 전송은 파일·URL만 지원하고 일반 텍스트·유형 탭은 제거하며 통합 전송 수정안을 우선한다.
1. 아키텍처
apps/web: Vue 3 + JavaScript + Vite + Tailwind CSS. 기기 홈, 보내기 패널, 수신 요청 카드, 이번 기기, 계정 보관함, 설정, 관리자 화면. REST 재조회로 상태를 복구하며 Socket.IO 이벤트는 변경 알림으로 사용한다. 사용자 요청에 따라 프런트엔드부터 TypeScript를 제거했고, 공통 모듈과 서버는 후속 단계에서 전환한다.apps/server: Fastify + TypeScript. 설정, 인증, 발견, 연결 공간, 요청, 전송, 접근권, 저장소, 용량, 정리, 메일, 운영 모듈을 분리한다. HTTP와 Socket.IO는 동일한 인증·권한 서비스를 사용한다.packages/shared: 입력 검증 스키마, API DTO, 오류 코드, 상태와 이벤트 타입. 서버 비밀값이나 DB 내부 모델을 프런트엔드에 내보내지 않는다.- SQLite + Drizzle: 버전 관리된 마이그레이션, 외래 키, WAL, busy timeout. 원자적 상태 전이와 용량 예약은 짧은 쓰기 트랜잭션으로 처리한다. 스트리밍이나 SMTP 호출 중 DB 트랜잭션을 유지하지 않는다.
- NAS 비공개 볼륨: 임시 업로드와 확정 파일 분리. 임의 저장 키만 사용하며 정적 파일 라우트로 노출하지 않는다.
- Docker Compose: 단일 앱 서버가 빌드된 웹과 API를 제공한다. DB와 파일 볼륨을 분리하고 Nginx Proxy Manager가 HTTPS를 종료한다. 배포 전 자동 마이그레이션을 수행한다.
- 초기에는 단일 프로세스의 메모리에서 활성 소켓·발견 그룹·속도 제한을 관리한다. DB가 요청과 전송의 기준이다. 재시작 시 모든 기기는 재연결 전까지 오프라인이다. 다중 프로세스 배포는 초기 범위에서 제외한다.
2. 인증과 권한 경계
익명 세션과 계정 세션은 서로 다른 HttpOnly 쿠키를 사용한다. 충분한 길이의 무작위 비밀값을 발급하고 DB에는 해시만 저장한다. 공개 device ID는 권한 증명이 아니다. 운영 쿠키에는 Secure와 SameSite=Lax를 적용한다.
익명 세션 하나에 브라우저 기기 하나가 연결된다. 여러 탭은 같은 세션을 공유하며 기기별 소켓 집합으로 온라인을 계산한다. 마지막 소켓 종료 뒤 짧은 유예 후 오프라인으로 처리한다. 초기 세션 발급은 여러 탭의 첫 방문 경쟁으로 중복 기기가 만들어지지 않도록 클라이언트에서 직렬화한다.
계정 로그인은 익명 기기를 교체하지 않는다. 자료 조회는 item_access를 기준으로 현재 주체의 권한·삭제·만료를 확인한다. 로그인만으로 익명 자료를 계정에 합치지 않는다. 계정 저장은 명시적 동작이며 원래 만료를 유지한다.
상태 변경에는 CSRF 토큰과 Origin 검증을 적용한다. Socket.IO 연결도 세션과 Origin을 확인하고 서버가 room을 결정한다. 권한 상실 시 연결 또는 해당 room을 제거한다. 콘텐츠·토큰·원본 IP·민감 URL은 로그와 이벤트에 포함하지 않는다.
같은 네트워크 탐색에는 직접 접속 IP 또는 명시적으로 신뢰한 프록시에서 검증한 IP만 사용한다. 정확한 주소별 HMAC 그룹을 접속 중 메모리에 유지한다. IPv6 /64 통합과 임의 X-Forwarded-For 신뢰는 금지한다. 배포 경로를 검증하지 못하면 탐색을 비활성화하고 QR·코드를 제공한다.
3. 데이터베이스
내부 ID는 UUID, 시간은 UTC로 저장한다. 다음 관계를 단계별 마이그레이션으로 추가한다.
| 영역 | 테이블 | 관계·주요 제약 |
|---|---|---|
| 익명 기기 | guest_sessions, browser_devices | 세션 해시 UNIQUE, 기기의 guest_session_id UNIQUE, 이름 길이·발견 설정, 만료 인덱스 |
| 공간 | connection_spaces, space_members | 생성 주체, QR·코드 해시, 만료; 공간/기기 UNIQUE, 승인·퇴장 상태 |
| 계정 | users, account_sessions, account_tokens | 정규화 이메일 UNIQUE, 세션/토큰 해시 UNIQUE, 역할·상태, 토큰 목적·만료·사용 시각 |
| 요청 | transfer_requests | local/email, 발신·수신 주체, 불변 메타데이터, 요청 상태, 승인·만료 시각 |
| 저장 | transfer_batches, items, files | 요청 참조, 전송 상태, 항목별 결과, 파일 키 UNIQUE, 실제 바이트·체크섬·임시 상태 |
| 접근권 | item_access | 항목/주체/역할 UNIQUE, 읽음·즐겨찾기·삭제·접근 만료 |
| 수신 정책 | user_rules | 계정 간 허용/차단 UNIQUE; 정책은 계정 설정에 저장 |
| 예약·삭제 | quota_reservations, deletion_operations | 주체·파일·예약 만료, 서버 복원 ID·5초 기한·삭제 대상 |
| 운영 | reports, system_settings, audit_events | 신고는 식별자·사유, 감사 기록은 최소 행위·결과·시간 |
명세에 필요한 보조 구조도 추가한다. 익명 차단 관계, 주체/작업/idempotency key별 요청 해시와 결과, 삭제 작업의 대상 접근권 관계를 별도 테이블로 관리한다. 이 구조는 익명 차단·중복 방지·다중 삭제 복원을 위한 내부 구현이다.
모든 주체 컬럼은 user_id와 guest_session_id 중 정확히 하나가 있는 CHECK를 적용한다. 요청의 수신 주체는 모드와 일치해야 한다. 목록에는 주체/삭제/생성 시각/ID 복합 인덱스를, 정리에는 상태/만료 인덱스를 둔다. 요청 메타데이터는 승인 후 수정할 수 없다.
사용자 탈퇴와 익명 초기화는 해당 접근권을 폐기한다. 상대방의 유효한 자료까지 연쇄 삭제하지 않는다. 발신자 식별 제거와 저장 원본 소유권·용량 정리는 별도 절차로 처리한다.
4. API 계약
명세의 경로를 유지한다. 추가로 상태 변경을 위한 CSRF 토큰 전달 계약과 익명 차단 동작을 정의한다. 입력은 공유 스키마로 검증하고 응답에는 저장 키·세션 해시·프록시 정보가 포함되지 않는다.
| API 영역 | 인증·권한 | 도입 단계 |
|---|---|---|
| guest/session, device/me, device/reset | 세션 발급 외에는 유효한 익명 세션 | 1 |
| discovery/devices, spaces 및 참여 승인·퇴장 | 익명 세션, 발견 범위 또는 생성자 권한 | 1 |
| transfer-requests 생성·조회·수락·거절·취소 | 해당 요청의 송신/수신 주체, 발견 범위 재검사 | 1, 이메일은 3 |
| transfers/:id/complete | 승인된 요청의 송신 주체, 불변 메타데이터·상태 검사 | 1, 파일은 2 |
| device/items, access 상세·read | 해당 익명 접근권; 상세와 사용 동작에서만 읽음 | 1 |
| uploads 생성·취소, download, preview | 예약과 요청에 결합한 업로드 권한 또는 자료 접근권 | 2 |
| delete-batch, deletion-operations/:id/undo | 본인 접근권·본인 삭제 작업·서버 유예 기한 | 2 |
| auth, account, inbox, vault, sent, favorites, rules | 인증 계정; 원격 사용에는 이메일 인증 필수 | 3, 세션·탈퇴 완성은 4 |
| access save-to-account, favorite | 익명 원본 접근권과 인증 계정 또는 계정 접근권 | 3 |
| resend, reports, admin | 본인 접근권/인증 계정/관리자 역할 | 4 |
| health | 공개 최소 상태; 내부 경로나 설정은 비공개 | 1 |
오류는 { code, message, requestId } 형식이다. 목록은 limit과 불투명 cursor를 받으며 동일 시각 자료도 안정적으로 순회하도록 시각과 ID를 함께 정렬한다. read/favorite은 토글 대신 목표 boolean을 받는다.
요청 생성·수락·전송 확정은 주체와 작업에 결합한 Idempotency-Key를 사용한다. 같은 키/같은 내용은 같은 결과, 다른 내용은 409 충돌로 처리한다. 저장된 결과를 반환해도 현재 접근권을 확인한다.
Socket.IO는 명세의 device, space, request, transfer, inbox, access, account 이벤트를 제공한다. 이벤트는 서버가 범위를 검사한 세션에만 보낸다. 재연결과 탭 복귀에는 목록을 REST로 재조회한다. 파일 바이트는 HTTP 스트리밍으로 전달한다.
5. 요청·전송 상태
요청은 pending → accepted/rejected/expired/cancelled로 전이한다. 수락은 수신 주체만, 대기 요청 취소는 발신 주체만 가능하다. 중복 요청은 기존 결과를 반환하고 충돌 전이는 거절한다. 수락 시 온라인·발견 허용·범위·차단·만료·용량을 재검사한다.
전송은 waiting → uploading → stored/failed이며 항목별 상태를 별도로 둔다. 즉석 텍스트와 URL도 요청에는 유형·바이트 크기·개수만 보내고 본문은 수락 뒤 확정 API로 보낸다. URL은 개별 항목으로 저장한다. URL이나 텍스트 내용이 변경되면 새 요청을 만드는 클라이언트 흐름을 제공한다.
파일 요청은 불변 파일 목록에 결합한다. 수락과 예약 후 목록에 있는 파일만 업로드한다. 성공 파일은 유지하고 실패 파일만 같은 메타데이터로 다시 업로드한다. 실제 바이트와 선언 크기가 다르면 확정하지 않는다. 업로드 취소·실패·시간 초과는 예약과 임시 파일을 회수한다.
이메일 전송은 인증 계정 대상으로만 가능하다. 자기 주소와 허용 관계는 승인된 상태에서 시작한다. 낯선 발신자는 최대 7일 수락 대기 후 승인되면 24시간 내 업로드를 시작해야 한다. 오프라인 송신자의 로컬 파일은 서버에서 자동 전달할 수 없으므로 재선택을 안내한다. 시작·확정 시 수신 정책을 재검사한다.
6. 용량과 정리
용량 제한과 예약은 같은 DB 쓰기 트랜잭션에서 검사한다. 실제 파일은 원본 송신자의 논리 사용량에, 수신자 즐겨찾기는 해당 수신자의 논리 사용량에 반영한다. 자기 저장은 중복 합산하지 않는다. 익명 전체 상한·동시 업로드·최소 디스크 여유도 검사한다. MiB/KiB와 GiB는 이진 단위로 계산한다.
자료의 기본 만료는 확정 시각부터 텍스트·URL 30일, 파일 7일이다. 즐겨찾기는 해당 계정의 접근권만 보존한다. 읽음과 상대의 즐겨찾기는 본인 만료를 연장하지 않는다. 익명 세션이 먼저 만료되면 자료가 남아 있어도 이전 세션으로 접근하지 못한다.
삭제는 접근권 단위로 수행하고 5초 서버 유예 동안 복원 가능하다. 자연 만료는 복원하지 않는다. 유예 중 물리 정리를 보류한다. 마지막 유효 접근권·즐겨찾기·복원 유예·재전송 참조가 종료된 이후에만 원본을 정리한다.
주기 작업은 요청·공간·세션 만료, 예약 회수, 24시간 고아 업로드, 접근권·자료 만료, 파일 참조 및 용량 불일치, 30일 감사 기록을 처리한다. 작업은 중복 실행에 안전하게 만들고 실패를 운영 상태에 남긴다. DB 삭제와 파일 unlink의 실패는 재시도 가능한 정리 상태로 관리한다.
7. UI
홈은 hop., 내 기기 이름 편집, 발견 허용, 같은 네트워크 기기, QR·코드 연결, 통합 보내기 영역으로 구성한다. 첫 방문 모달·필수 이름 입력·권한 요청은 없다. 이름은 중복 가능한 비인증 표시임을 고려해 서버 구분 코드를 병기한다.
데스크톱은 기기 카드와 전송 패널, 모바일은 단일 열과 하단 기기/이번 기기/보관함 내비게이션을 사용한다. 조용한 수신 요청 카드에서 메타데이터만 표시하고 수락 뒤 콘텐츠를 제공한다. 자동 링크 열기·다운로드는 없다.
44px 이상의 터치 목표, 키보드·스크린리더 동작, 대비와 reduced motion을 지원한다. 스와이프 삭제는 버튼을 드러내는 동작이며 세로 스크롤과 버튼·텍스트 선택을 방해하지 않게 한다. 반응형 자동 테스트와 실기기 검증은 별도로 보고한다.
8. 단계별 구현과 완료 관문
| 단계 | 결과 | 빌드·테스트 관문 |
|---|---|---|
| 1 | 모노레포·Docker·익명 세션·이름·발견·공간·온라인·텍스트/URL 요청과 이번 기기 | 타입 검사와 전체 빌드, 실제 SQLite API 통합 테스트, 두 브라우저 문맥 전송, 다중 탭·범위 격리·승인·퇴장·차단·타임아웃·중복·CSRF·Origin 테스트 |
| 2 | 스트리밍 파일·진행률·취소·인증 다운로드/래스터 미리보기·용량·정리·삭제/복원 | Phase 1 회귀, 수락 전 업로드 차단, 선언 크기 불일치·중단·경로 조작·동시 예약, 권한 없는 다운로드, 부분 실패 재시도, 5초 복원·만료·터치 동작 |
| 3 | SMTP 인증·로그인/복구·계정 보관함·수신 정책·오프라인 전송·계정 용량·익명 선택 저장 | 기존 단계 회귀, 테스트 SMTP 메일과 일회용 토큰, 세션 회전·재설정 무효화, 미인증 차단·허용/차단 우선순위·계정 격리·다중 기기 동기화 |
| 4 | PWA·공유 초안·SSRF 방어 미리보기·테마·비활성 광고 슬롯·재전송·세션/탈퇴·신고/관리자·운영 문서 | 전체 빌드·회귀, private API 캐시 제외, 계정 전환, DNS/redirect SSRF 차단, 광고 미활성, 관리자 권한, 탈퇴 후 상대 자료 보존 |
| 5 | 보안·복구·모바일·배포 검증과 완료 기준별 증거 | 공격성 통합 테스트, 실제 프록시 경로 검증, Docker 재시작·볼륨 보존·일관 백업/복원, 모바일/PWA 실기기 확인 |
각 단계에서 코드·마이그레이션·문서·환경 예시를 갱신하고 테스트 명령과 결과를 기록한다. 이전 단계의 관문을 통과한 후 다음 단계로 진행한다. 테스트에만 쓰는 fixture와 실제 구현 완료 증거를 구분한다. 사용할 수 없는 Docker daemon이나 실기기는 검증하지 않은 항목으로 명시한다.
9. 구현 전 확인 사항
Phase 1 배포 IP 신뢰 정책을 위해 실제 접속 경로 확인이 필요하다. 인터넷 → Nginx Proxy Manager → hop.인지, Cloudflare나 추가 프록시가 있는지 확인한다. 신뢰할 프록시 IP/CIDR은 환경 설정으로 명시한다. 검증 전 운영 환경의 같은 네트워크 탐색은 끈다.
이후 단계에서는 실제 SMTP·최초 관리자 비밀값·운영 비밀값을 코드나 Git에 넣지 않고 운영자가 주입한다. 개발 검증에는 격리된 SMTP를 사용한다. 광고 사업자 연동과 공개 활성화는 해당 시점의 정책 검토 뒤 진행한다.
2026-10-12 구조 수정
최신 기기 중심 화면, 로그인 수신 기록, 7일 임시 보관함, 메모와 저장 자료 전달, 단일 방 격리는 TEMPORARY_VAULT.md를 기준으로 구현했다. 외부 이메일 원격 전송과 영구 즐겨찾기, 익명 사후 가져오기는 이전 계획에서 제외한다. 신규 보관함 서비스·테스트 코드는 JavaScript이며 서버의 기존 TypeScript는 후속 전환 단계까지 유지한다.