diff --git a/README.md b/README.md index 29ed07f..e36baaa 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,233 @@ -
+Slide 16_9 - 47 + +

+ zipzip +

+ +> 메인폰 밖에서 촬영된 사진을 더 쉽게 찾고, 분류하고, 다시 볼 수 있도록 돕는 사진 정리 보조 서비스 + +서브폰·카메라 등 여러 기기로 촬영한 사진이 한 라이브러리에 섞여 있으면 원하는 사진을 다시 찾기 어렵습니다. **zipzip**은 사용자의 사진 라이브러리를 분석해 **촬영 기기·날짜·장소 기준으로 사진을 자동 인덱싱**하고, 이를 바탕으로 필터링·앨범 정리·그룹 공유까지 이어지는 정리 경험을 제공하는 iOS 앱입니다. + +## ✨ 주요 기능 + +| 기능 | 설명 | +| --- | --- | +| 인증 | Sign in with Apple 로그인, JWT Access/Refresh Token 발급·회전, 로그아웃 | +| 사용자 | 내 프로필 조회·수정, 회원 탈퇴 | +| 공유 그룹 | 그룹 생성·조회·수정·삭제, 초대 코드 미리보기·참여·탈퇴, 멤버 조회와 역할 기반 권한 관리 | +| 공유집(앨범) | 그룹별 앨범 생성·조회·수정·삭제, 여러 앨범 일괄 삭제 | +| 사진 | Object Storage 직접 업로드용 presigned URL 발급, 업로드 완료 등록, 조회·메타데이터 수정·삭제 | +| 사진 분류 | 하나의 사진을 여러 앨범에 첨부하거나 분리하는 N:M 구조 | +| 반응 | 사진 상세 조회, 좋아요 설정·취소, 댓글 조회·작성 | +| 채팅 | 공유 그룹의 텍스트 메시지와 사진 댓글 활동을 하나의 커서 기반 타임라인으로 조회 | +| 데이터 수명주기 | 주요 리소스 soft delete, 만료 데이터·스토리지 객체·실패한 썸네일의 주기적 정리 및 재처리 | + +### 설계 방향 + +- **제어 평면과 데이터 평면 분리:** API 서버는 권한·메타데이터·임시 URL을 관리하고, 사진 원본은 iOS 클라이언트가 Object Storage에 직접 업로드합니다. +- **DB를 진실의 원천으로 사용:** PostgreSQL의 상태를 기준으로 처리하며, 스토리지와의 결과적 일관성은 주기적 스윕으로 보완합니다. +- **무거운 작업은 비동기로 처리:** 썸네일 생성은 제한된 전용 스레드 풀에서 실행하고, 실패하거나 유실된 작업은 DB 상태를 바탕으로 재시도합니다. +- **REST와 폴링 중심의 단순한 구조:** 별도 WebSocket 계층 없이 커서 기반 증분 조회로 채팅과 활동 피드를 제공합니다. +- **공개 API 계약 우선:** iOS 팀이 Swagger와 `docs/apidoc/` 문서를 API 명세로 사용할 수 있도록 요청·응답과 오류 코드를 함께 관리합니다. + +## 🛠 기술 스택 + +### Application + +

+ Java 21 + Spring Boot 3.5.15 + Spring Web MVC + Spring Security +

+ +### Data & Storage + +

+ Spring Data JPA + PostgreSQL 18 + Flyway + OCI Object Storage + AWS SDK for Java +

+ +### API & Quality + +

+ OpenAPI 3 + Swagger UI + JUnit 5 + Testcontainers + Spotless +

+ +### Infrastructure & Collaboration + +

+ Docker + Nginx + GitHub Actions + Oracle Cloud + GitHub +

+ +### 기술 선정 이유 + +| 구분 | 기술 | 선정 이유 | +| --- | --- | --- | +| 언어 | Java 21 | 장기 지원 버전과 Spring 생태계를 활용하고 최신 JVM 기능을 사용할 수 있습니다. | +| 프레임워크 | Spring Boot 3.5 | 웹·보안·검증·데이터 접근을 일관된 구성으로 제공하며 팀 내 생산성이 높습니다. | +| 데이터베이스 | PostgreSQL | 관계 무결성, 트랜잭션, 고급 인덱스와 제약조건으로 공유 데이터의 정합성을 보장합니다. | +| 스키마 관리 | Flyway | 애플리케이션과 함께 버전별 스키마 변경 이력을 재현할 수 있습니다. | +| 파일 저장 | OCI Object Storage | 대용량 원본을 서버가 중계하지 않고 S3 호환 presigned URL로 직접 전송할 수 있습니다. | +| 인증 | Sign in with Apple + JWT | iOS 사용자에게 자연스러운 로그인 경험을 제공하고 서버 세션을 stateless하게 유지합니다. | +| API 문서 | springdoc-openapi | 코드와 Swagger 문서의 간극을 줄이고 iOS 팀이 실행 가능한 계약을 확인할 수 있습니다. | +| 테스트 | JUnit 5 + Testcontainers | 순수 단위 테스트와 실제 PostgreSQL 기반 통합 테스트를 목적에 맞게 분리할 수 있습니다. | +| 배포 | Docker + GitHub Actions | 동일한 이미지를 검증·배포하고 헬스체크 실패 시 이전 이미지로 되돌릴 수 있습니다. | + +## 🏗️ 시스템 아키텍처 + +Zipzip 시스템 아키텍처 + +## 🗄️ DB 설계 + +최신 ERD와 테이블 관계는 dbdocs에서 확인할 수 있습니다. + +[![dbdocs](https://img.shields.io/badge/dbdocs-View%20ERD-4F46E5?style=for-the-badge)](https://dbdocs.io/embed/cf956a90274133517f45d2affd1add14/ecdf7220e60f4c4887f38ca90ab7ef21) + +## 📁 폴더 구조 + +```text +zipzip-server/ +├── src/ +│ ├── main/ +│ │ ├── java/org/zipzip/zipzipserver/ +│ │ │ ├── domain/ +│ │ │ │ ├── auth/ # Apple 로그인, JWT, Refresh Token +│ │ │ │ ├── user/ # 사용자 프로필과 탈퇴 +│ │ │ │ ├── sharedgroup/ # 공유 그룹, 초대, 멤버 +│ │ │ │ ├── album/ # 공유집(앨범)과 사진 매핑 +│ │ │ │ ├── photo/ # 사진 업로드·조회·썸네일 +│ │ │ │ ├── reaction/ # 좋아요와 댓글 +│ │ │ │ ├── chat/ # 그룹 채팅·활동 타임라인 +│ │ │ │ └── storage/ # Object Storage 추상화와 S3 구현 +│ │ │ └── global/ # 보안, 공통 응답, 예외, 커서, 멱등성 +│ │ └── resources/ +│ │ ├── application.yaml +│ │ ├── config/ # 비공개 설정 Git 서브모듈 +│ │ └── db/migration/ # Flyway 마이그레이션 +│ └── test/ # 단위·통합·계약·벤치마크 테스트 +├── docs/ +│ ├── apidoc/ # API 계약과 설계 결정 +│ ├── architecture/ # 백엔드 아키텍처 +│ └── data-modeling/ # 용어, 모델, 데이터 사전, DBML +├── deploy/ # 운영·개발 Docker Compose와 Nginx 설정 +├── postman/ # API 요청 컬렉션 +├── docker-compose.yml # 로컬 PostgreSQL +└── build.gradle +``` + +## 🚀 로컬 실행 + +### 요구 사항 + +- JDK 21 +- Docker 및 Docker Compose +- 비공개 설정 저장소 `zipzip-server-config` 접근 권한 - +### 1. 저장소와 설정 내려받기 -
+```bash +git clone --recurse-submodules https://github.com/zipzip-team/zipzip-server.git +cd zipzip-server +``` -### `MEMBERS` +이미 저장소를 복제했다면 서브모듈을 별도로 초기화합니다. + +```bash +git submodule update --init --recursive +``` + +### 2. PostgreSQL 실행 + +```bash +docker compose \ + --env-file src/main/resources/config/docker-compose.env \ + up -d +``` + +`application-secret.yml`의 datasource 설정과 `docker-compose.env`의 DB 설정은 서로 일치해야 합니다. 민감한 설정 파일은 메인 저장소에 커밋하지 않습니다. + +### 3. 서버 실행 + +```bash +./gradlew bootRun +``` + +서버가 실행되면 다음 주소에서 상태와 API 문서를 확인할 수 있습니다. + +- Health Check: [http://localhost:8080/actuator/health](http://localhost:8080/actuator/health) +- Swagger UI: [http://localhost:8080/swagger-ui/index.html](http://localhost:8080/swagger-ui/index.html) +- OpenAPI JSON: [http://localhost:8080/v3/api-docs](http://localhost:8080/v3/api-docs) + +### 4. 검증 + +```bash +./gradlew check +``` + +```bash +./gradlew spotlessApply +./gradlew spotlessCheck +``` + +Gradle 명령은 JDK 21로 실행해야 합니다. 로컬 기본 JDK가 다른 경우 `JAVA_HOME`을 JDK 21 경로로 지정합니다. + +## 📚 문서 + +| 문서 | 내용 | +| --- | --- | +| [API 명세 인덱스](docs/apidoc/00-api-index.md) | API별 계약 문서와 구현 상태 | +| [공통 API 규격](docs/apidoc/01-common-spec.md) | 인증, 공통 응답, 오류, 페이지네이션 규칙 | +| [백엔드 아키텍처](docs/architecture/backend-architecture.md) | 핵심 설계 결정과 런타임·배포 구조 | +| [데이터 모델링](docs/data-modeling/04-data-modeling.md) | 엔티티 관계와 데이터 모델 | +| [데이터 사전](docs/data-modeling/05-data-dictionary.md) | 테이블·컬럼·제약조건 정의 | +| [코드 포매팅](docs/code-formatting.md) | Spotless와 Java 포맷 규칙 | +| [배포 파이프라인](docs/deployment-pipeline.md) | CI/CD 구성과 배포 흐름 | + +## 🤝 Convention + +### Branch + +일반 작업 브랜치는 `/-` 형식을 사용합니다. + +```text +feat/2-user-login +fix/15-refresh-token-expiration +chore/42-update-dependencies +``` + +`main`, `develop`에는 직접 push하지 않고 작업 브랜치에서 Pull Request를 생성합니다. + +### Commit Message + +커밋 메시지는 `:: : <한국어 요약>` 형식을 사용합니다. + +```text +:sparkles: feat: Apple 로그인 기능 구현 +:bug: fix: Refresh Token 만료 처리 수정 +:memo: docs: API 명세 업데이트 +``` + +커밋 전 저장소의 Git hook을 사용하도록 설정합니다. + +```bash +git config core.hooksPath .githooks +``` + +## 👥 Members + +
@@ -13,10 +236,10 @@ 윤해민
윤해민
- + Server Lead

- + hamtorygoals
@@ -24,17 +247,15 @@ 신재훈
신재훈
- + Server

- + jaehunshin-git
-
- © 2026 Team Zipzip
diff --git a/docs/architecture/zipzip-system-architecture-diagram.png b/docs/architecture/zipzip-system-architecture-diagram.png new file mode 100644 index 0000000..5151983 Binary files /dev/null and b/docs/architecture/zipzip-system-architecture-diagram.png differ diff --git a/docs/data-modeling/04-data-modeling.md b/docs/data-modeling/04-data-modeling.md index 20d4028..ed15e54 100644 --- a/docs/data-modeling/04-data-modeling.md +++ b/docs/data-modeling/04-data-modeling.md @@ -442,6 +442,7 @@ PostgreSQL 보완 인덱스: - [x] 사진 원본·썸네일을 URL이 아닌 Object Storage 객체 키(`original_object_key`, `thumbnail_object_key`)로 저장하도록 반영했다. - [x] `photo.original_object_key`에 unique 제약을 적용했다. - [x] 비동기 썸네일 생성 진행 상태를 `photo.thumbnail_status`(`PENDING`/`READY`/`FAILED`)로 반영했다. +- [x] `photo.thumbnail_status`가 `READY`이면 공백이 아닌 `thumbnail_object_key`를 갖도록 DB 제약을 적용했다. - [x] `photo_upload_reservation`을 추가해 업로드 URL 발급 대상(사용자·공유집(앨범))과 재사용 여부를 완료 등록에서 검증할 수 있도록 했다. - [x] 사진 댓글과 그룹 채팅 메시지를 soft delete에서 즉시 물리 삭제로 전환하고 `deleted_at` 컬럼을 제거했다. - [x] 사진·공유집(앨범)의 개별 삭제 후 30일 정리를 위한 기준을 정의했다. @@ -449,6 +450,7 @@ PostgreSQL 보완 인덱스: - [x] 탈퇴한 생성자·업로더의 공유집(앨범)·사진 삭제 권한을 공유 그룹 방장에게 위임했다. - [x] 사용자 탈퇴 시 사진 좋아요 물리 삭제 정책을 정의했다. - [x] 활성 멤버십 기준에 사용자 soft delete 상태를 포함했다. +- [x] API 멱등성 처리 기록(`api_idempotency_record`)의 상태·완료 응답·만료 정리 모델을 반영했다. - [x] 공유 그룹 삭제 시 하위 공유집(앨범)·사진을 함께 soft delete하고, Object Storage 우선 정리 뒤 그룹·초대 코드 예약을 물리 삭제하는 30일 배치를 반영했다. ## 15. 남은 구현 과제 diff --git a/docs/data-modeling/05-data-dictionary.md b/docs/data-modeling/05-data-dictionary.md index f780b13..dccff1f 100644 --- a/docs/data-modeling/05-data-dictionary.md +++ b/docs/data-modeling/05-data-dictionary.md @@ -10,6 +10,7 @@ | 테이블 | 한글명 | 설명 | |---|---|---| | `app_user` | 사용자 | Apple 로그인 후 서버에 등록된 사용자 | +| `api_idempotency_record` | API 멱등성 처리 기록 | 멱등성 요청의 처리 상태와 완료 응답을 보관하는 기록 | | `refresh_token` | Refresh Token | Refresh Token 해시와 회전 상태 | | `invite_code_reservation` | 초대 코드 예약 원장 | 발급된 초대 코드의 점유 예약 테이블 | | `shared_group` | 공유 그룹 | 초대 코드, 멤버십, 채팅, 공유집(앨범)을 묶는 최상위 공유 공간 | @@ -221,6 +222,7 @@ PHOTO-03 완료 등록은 이 테이블에서 요청 사용자·요청 경로 - `original_object_key` unique - `thumbnail_status`는 `PENDING`, `READY`, `FAILED`만 허용(`chk_photo__thumbnail_status`) +- `thumbnail_status`가 `READY`이면 `thumbnail_object_key`는 공백이 아닌 값이어야 함(`chk_photo__thumbnail_ready_object_key`) - 원본 수정과 삭제는 원칙적으로 업로더만 가능 - 업로더가 탈퇴한 경우 삭제는 공유 그룹 방장이 가능 - 항상 1개 이상의 `shared_album_photo` 매핑을 가져야 한다(서비스 계층에서 강제) @@ -279,6 +281,32 @@ PHOTO-03 완료 등록은 이 테이블에서 요청 사용자·요청 경로 - `content` 공백 불가 - 댓글 작성과 조회는 활성 공유 그룹 멤버십 필요 +### 3.13 `api_idempotency_record` + +멱등성 헤더를 사용하는 API 요청의 처리 상태와 완료 응답을 보관한다. +동일한 멱등성 키로 서로 다른 요청을 재사용하는 것을 방지하며, 만료된 기록은 정기 배치가 물리 삭제한다. + +| 컬럼 | 타입 | 필수 | 설명 | +|---|---|---|---| +| `id` | `uuid` | O | 멱등성 처리 기록 식별자 | +| `scope` | `varchar(100)` | O | 멱등성 키 업무 범위 | +| `idempotency_key` | `uuid` | O | 클라이언트가 전달한 멱등성 키 | +| `http_method` | `varchar(10)` | O | 요청 HTTP 메서드 | +| `api_path` | `varchar(255)` | O | 멱등성 적용 API 경로 | +| `request_hash` | `varchar(64)` | O | 요청 본문 충돌 판별용 SHA-256 해시 | +| `status` | `varchar(20)` | O | `PROCESSING` 또는 `COMPLETED` | +| `response_http_status` | `integer` | X | 완료된 요청의 HTTP 응답 상태 코드 | +| `encrypted_response_body` | `text` | X | 완료된 요청의 암호화된 응답 본문 | +| `expires_at` | `timestamptz` | O | 기록 만료 시각 | +| `created_at` | `timestamptz` | O | 생성 시각 | +| `updated_at` | `timestamptz` | O | 상태 마지막 갱신 시각 | + +주요 제약: + +- `scope`, `idempotency_key`, `http_method`, `api_path` unique +- `status`는 `PROCESSING`, `COMPLETED`만 허용 +- `PROCESSING`이면 응답 상태 코드와 암호화된 응답 본문은 모두 `null`, `COMPLETED`이면 둘 다 필수 + ## 4. 외래 키 | 제약명 | 관계 | 삭제 규칙 | @@ -319,6 +347,8 @@ PHOTO-03 완료 등록은 이 테이블에서 요청 사용자·요청 경로 | `photo_upload_reservation` | `idx_photo_upload_reservation__shared_album_id` | 공유집(앨범)별 예약 조회 | | `photo_upload_reservation` | `idx_photo_upload_reservation__requested_by_app_user_id` | 사용자별 예약 조회 | | `photo_upload_reservation` | `idx_photo_upload_reservation__expires_at` | 만료된 미완료 예약 스윕 | +| `api_idempotency_record` | `uk_api_idempotency_record__scope_key_method_path` | 동일 요청의 중복 처리 방지 | +| `api_idempotency_record` | `idx_api_idempotency_record__expires_at` | 만료된 멱등성 처리 기록 정리 | ## 6. 권한 기준 diff --git a/docs/data-modeling/dbml/zipzip.dbml b/docs/data-modeling/dbml/zipzip.dbml index e5bf98c..00aa382 100644 --- a/docs/data-modeling/dbml/zipzip.dbml +++ b/docs/data-modeling/dbml/zipzip.dbml @@ -23,6 +23,33 @@ Table app_user { Note: 'Apple 로그인 후 서버에 등록된 사용자. 탈퇴 시 deleted_at을 기록하고 display_name을 탈퇴한 사용자로 갱신한다. 공유 콘텐츠의 작성자·생성자·업로더 표시는 탈퇴한 사용자로 대체한다. 재가입 복구를 위해 app_user 행은 물리 삭제하지 않는다. 동일한 Apple 계정으로 재가입하면 기존 행을 복구하되 과거 공유 그룹 멤버십은 자동 복구하지 않는다.' } +Table api_idempotency_record { + id uuid [pk, not null, note: '애플리케이션에서 생성하는 멱등성 처리 기록 식별자'] + scope varchar(100) [not null, note: '멱등성 키의 업무 범위. 사용자·그룹 등 요청을 구분하는 기준'] + idempotency_key uuid [not null, note: '클라이언트가 전달한 멱등성 키'] + http_method varchar(10) [not null, note: '요청 HTTP 메서드'] + api_path varchar(255) [not null, note: '멱등성 적용 API 경로'] + request_hash varchar(64) [not null, note: '동일 멱등성 키의 요청 본문 충돌을 판별하는 SHA-256 해시'] + status varchar(20) [not null, note: '처리 상태. PROCESSING 또는 COMPLETED'] + response_http_status int [note: '완료된 요청의 HTTP 응답 상태 코드. 처리 중이면 null'] + encrypted_response_body text [note: '완료된 요청의 암호화된 응답 본문. 처리 중이면 null'] + expires_at timestamptz [not null, note: '멱등성 기록 만료 시각. 만료 후 정리 배치가 삭제한다'] + created_at timestamptz [not null, default: `now()`, note: '처리 기록 생성 시각'] + updated_at timestamptz [not null, default: `now()`, note: '처리 상태 마지막 갱신 시각'] + + indexes { + (scope, idempotency_key, http_method, api_path) [unique, name: 'uk_api_idempotency_record__scope_key_method_path'] + expires_at [name: 'idx_api_idempotency_record__expires_at'] + } + + checks { + `status in ('PROCESSING', 'COMPLETED')` [name: 'chk_api_idempotency_record__status'] + `(status = 'PROCESSING' and response_http_status is null and encrypted_response_body is null) or (status = 'COMPLETED' and response_http_status is not null and encrypted_response_body is not null)` [name: 'chk_api_idempotency_record__completed_response'] + } + + Note: '멱등성 헤더가 필요한 API의 처리 상태와 완료 응답을 보관한다. 동일한 scope, idempotency_key, HTTP 메서드, API 경로 조합은 하나만 존재할 수 있으며, 만료된 행은 정기적으로 물리 삭제한다.' +} + Table refresh_token { id uuid [pk, not null, note: '애플리케이션에서 생성하는 Refresh Token 행 식별자'] app_user_id uuid [not null, note: 'Refresh Token을 소유한 사용자 식별자'] @@ -182,6 +209,7 @@ Table photo { checks { `thumbnail_status in ('PENDING', 'READY', 'FAILED')` [name: 'chk_photo__thumbnail_status'] + `thumbnail_status <> 'READY' or (thumbnail_object_key is not null and thumbnail_object_key !~ '^[[:space:]]*$')` [name: 'chk_photo__thumbnail_ready_object_key'] } Note: '사진 원본 파일 참조 정보. 공유 그룹에 직접 속하지 않고 shared_album_photo를 통해서만 하나 이상의 공유집(앨범)에 속한다(공유 위계는 공유 그룹 > 공유집(앨범) > 사진이다). 원칙적으로 업로더가 수정·삭제하며, 업로더가 탈퇴한 경우 삭제는 공유 그룹 방장이 할 수 있다. 클라이언트는 presigned PUT URL로 원본을 Object Storage에 직접 업로드하고, 서버는 업로드 완료 등록 시점에 이 행을 생성한다. iOS가 이미지 EXIF에서 촬영일시와 위치정보를 추출해 전달하면 그대로 저장하며, 위치정보가 추론된 값이면 is_inferred를 true로 전달받는다. 썸네일은 등록 직후 서버가 비동기로 생성한다. 촬영일 우선 정렬용 coalesce 표현식 partial index는 postgresql-overrides.sql에 정의한다.' diff --git a/zipzip-current-architecture.svg b/zipzip-current-architecture.svg new file mode 100644 index 0000000..bd4230a --- /dev/null +++ b/zipzip-current-architecture.svg @@ -0,0 +1,177 @@ + + Zipzip current service architecture + Repository-derived architecture showing GitHub Actions, Docker Hub, OCI Compute with Docker Compose, Nginx, Certbot, Spring Boot production and development containers, PostgreSQL, OCI Object Storage, and Apple Sign in. + + + + + + + + + + + + + + + + + + + + Zipzip — Current Service Architecture + Repository-derived deployment and runtime topology · A3 landscape + + + + + CI / CD PIPELINE + + + + GitHubRepository + + + + + GitHub ActionsCI/CD workflows + + + + + Docker Hubarm64 images + + + + + SSH Deployforced commandhealth check + rollback + + + + + + Secrets → stdin env file + + + + + + USERS / CLIENTS + + + + Zipzip iOS AppHTTPS API client + + + + + API Consumersapi.zipzip.site + + Direct object upload/downloaduses time-limited presigned URLs + + + + + OCI COMPUTE · AMPERE A1 (ARM64) + zipzip-be instance + + + Docker Compose · shared bridge network + + + + NginxTLS termination80 / 443 reverse proxy + + + + + CertbotLet’s Encryptrenews every 12 hours + + + + + + Production APIzipzip-be · Spring Boot 3.5 · Java 21api:8080 · JWT · Apple login · Flyway · JPAasync thumbnails · scheduled cleanup + + + + + + Development APIzipzip-be-dev · Spring Bootapi-dev:8080 · shared network + + + + + + EXTERNAL SERVICES + + + + Sign in with Appleidentity token verificationOAuth token exchange + + + + + Oracle Cloud InfrastructureS3-compatible Object Storagecustom endpoint · path-style access + + No API gateway or message queueis configured in this repository. + + + + + DATA / STORAGE LAYER + + + + PostgreSQL DatabaseSeparate database instanceapplication schema · Flyway · persistent data + + + + + OCI Object StorageS3-compatible bucketoriginal photos · thumbnails · signed URLs + + + + + Docker Hub Imageslatest · sha · dev tagspublic image pull at deployment time + + + + + SSH deployment + + HTTPS + + api.zipzip.site + + dev.api.zipzip.site + + certificate files + + Apple ID token + + JPA / JDBC + + S3 API + + presigned PUT / GET + + same codebase + + image pulled during deploy + + + + LEGEND + Deployment + Internal call + Data access + Routing + Authentication + Source: repository deployment configuration and application code + +