Skip to content

Commit c56b583

Browse files
Gimini-3claude
andcommitted
docs: #153 문서 목록 조회 API 설계 문서 추가
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 56d70d5 commit c56b583

1 file changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# 문서 목록 조회 API
2+
3+
closes #153
4+
5+
## 배경
6+
7+
문서 도메인에는 업로드(`POST /api/documents`)와 단건 인덱싱 상태 조회
8+
(`GET /api/documents/{documentId}/status`)만 있고, **사용자가 읽을 수 있는 문서 목록을
9+
반환하는 API가 없었다.** 문서를 한 번 업로드하고 나면 그 `documentId`를 따로 기억하지 않는 한
10+
다시 찾아갈 방법이 없어, 문서 화면을 구성할 수 없는 상태였다.
11+
12+
목록에 필요한 권한 판정은 이미 존재한다. 검색 pre-filter가 쓰는
13+
`DocumentRepository.findReadableDocumentIds`가 5가지 접근 경로(OWNER / PUBLIC /
14+
USER 캐시 / ROLE live / DEPARTMENT live)를 UNION으로 판정한다. 다만 이 쿼리는
15+
`d.status = 'INDEXED'`가 하드코딩되어 있어 그대로는 목록에 쓸 수 없다. 목록에서는 인덱싱 중
16+
(`INDEXING`)이거나 실패(`FAILED`)한 문서도 보여야 사용자가 진행 상황을 확인할 수 있기
17+
때문이다.
18+
19+
## 설계 판단
20+
21+
### 쿼리를 복제하지 않고 상태 조건만 파라미터화했다
22+
23+
UNION 7개 브랜치짜리 네이티브 쿼리를 목록용으로 복사하면 권한 정책이 두 벌이 되어, 이후 접근
24+
경로가 추가될 때 한쪽만 고치는 사고가 나기 쉽다. `d.status = 'INDEXED'`
25+
`d.status IN (:statuses)`로 바꾸고, 검색은 호출부에서 `INDEXED`만 넘겨 기존 동작을 그대로
26+
유지한다.
27+
28+
| 호출자 | 넘기는 상태 |
29+
|---|---|
30+
| `AccessibleDocumentQueryService` (검색) | `INDEXED` |
31+
| `DocumentQueryService` (목록) | `DELETED`를 제외한 전체, 또는 요청한 단일 상태 |
32+
33+
`statuses`는 네이티브 쿼리라 `DocumentStatus.name()` 문자열 목록으로 넘긴다.
34+
35+
### 권한 판정과 페이징을 분리했다
36+
37+
권한 pre-filter로 읽을 수 있는 문서 ID를 먼저 구하고, 그 ID 집합에 대해 JPQL 페이지 쿼리로
38+
정렬·페이징만 수행한다. 응답에 현재 버전 번호·상태를 담아야 하는데 `Document.currentVersion`
39+
`LAZY``LEFT JOIN FETCH`로 즉시 로딩한다(`countQuery`는 별도 지정).
40+
41+
읽을 수 있는 문서가 하나도 없으면 페이지 쿼리를 아예 실행하지 않고 빈 응답을 반환한다.
42+
43+
### 정렬은 고정이다
44+
45+
외부 `sort` 파라미터를 받지 않고 `createdAt DESC, id DESC`로 고정한다. 정렬 키를 열어두면
46+
인덱스 없는 컬럼 정렬 요청을 그대로 DB에 흘리게 되고, 응답 계약도 불안정해진다.
47+
48+
## API 명세
49+
50+
### 요청
51+
52+
```http
53+
GET /api/documents?status=INDEXED&page=0&size=20
54+
Authorization: Bearer {accessToken}
55+
```
56+
57+
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
58+
|---|---|---|---|---|
59+
| `status` | `DocumentStatus` | X | 없음 | 지정 시 해당 상태만 조회. 미지정 시 `DELETED` 제외 전체 |
60+
| `page` | int | X | `0` | 0부터 시작하는 페이지 번호 |
61+
| `size` | int | X | `20` | 페이지 크기, 1~100 |
62+
63+
`DocumentStatus`: `DRAFT` / `UPLOADED` / `INDEXING` / `INDEXED` / `FAILED` / `ARCHIVED` / `DELETED`
64+
65+
### 응답 200
66+
67+
```json
68+
{
69+
"success": true,
70+
"status": 200,
71+
"data": {
72+
"content": [
73+
{
74+
"documentId": 12,
75+
"title": "2026 상반기 운영 가이드",
76+
"description": "운영팀 공유용",
77+
"documentType": "PDF",
78+
"status": "INDEXED",
79+
"visibility": "PRIVATE",
80+
"ownerUserId": 3,
81+
"currentVersionNo": 2,
82+
"currentVersionStatus": "INDEXED",
83+
"createdAt": "2026-08-10T09:12:33",
84+
"updatedAt": "2026-08-11T14:02:10"
85+
}
86+
],
87+
"page": 0,
88+
"size": 20,
89+
"totalElements": 1,
90+
"totalPages": 1,
91+
"first": true,
92+
"last": true
93+
},
94+
"timestamp": "2026-08-12 19:30:00"
95+
}
96+
```
97+
98+
아직 인덱싱이 끝난 버전이 없으면 `currentVersionNo``currentVersionStatus``null`이다.
99+
100+
### 에러 케이스
101+
102+
| 상황 | HTTP | 응답 |
103+
|---|---|---|
104+
| 인증 토큰 없음 또는 만료 | 401 | 인증 실패 |
105+
| `page < 0`, `size < 1`, `size > 100` | 400 | 제약 조건 위반 |
106+
| `status`에 정의되지 않은 값 | 400 | 타입 변환 실패 |
107+
| 읽을 수 있는 문서 없음 | 200 | `content: []`, `totalElements: 0` (에러 아님) |
108+
109+
읽을 수 있는 문서가 없는 것은 정상 상태이므로 404가 아니라 빈 페이지를 반환한다. 권한 없는 문서는
110+
목록에서 조용히 제외되며, 존재 여부를 응답으로 노출하지 않는다.
111+
112+
## 변경 파일
113+
114+
| 파일 | 변경 |
115+
|---|---|
116+
| `DocumentRepository` | 권한 pre-filter 쿼리 2개 상태 파라미터화, 목록 페이지 쿼리 `findAllByIdIn` 추가 |
117+
| `AccessibleDocumentQueryService` | 검색 호출부에서 `INDEXED` 상태를 명시적으로 전달 |
118+
| `DocumentSummaryResponse` | 신규 응답 record |
119+
| `DocumentSummaryConverter` | 신규 Converter |
120+
| `DocumentQueryService` | `getMyDocuments` 추가 |
121+
| `DocumentQueryController` | 목록 API 추가, `@Validated`로 page·size 범위 검증 |
122+
123+
## 테스트
124+
125+
**단위 — `DocumentQueryServiceTest`**
126+
127+
- 읽을 수 있는 문서를 페이지 응답으로 변환해 반환한다
128+
- 읽을 수 있는 문서가 없으면 문서를 조회하지 않고 빈 페이지를 반환한다
129+
- `status`를 지정하지 않으면 `DELETED`를 제외한 전체 상태로 조회한다
130+
- `status`를 지정하면 해당 상태만으로 조회한다
131+
132+
**Repository — `DocumentReadableIdsRepositoryTest` (`@DataJpaTest`)**
133+
134+
- `INDEXED`만 요청하면 인덱싱 중인 문서는 제외한다
135+
- `INDEXING`을 함께 요청하면 인덱싱 중인 문서도 반환한다
136+
- soft delete된 문서는 `DELETED` 상태를 요청해도 제외한다
137+
- 컬렉션 범위 조회도 요청한 상태만 반환한다
138+
139+
seed 데이터에 PUBLIC·INDEXED 문서가 있어 모든 사용자 조회 결과에 포함되므로, 테스트가 생성한
140+
문서만 포함·제외로 검증한다.
141+
142+
**검색 회귀**
143+
144+
상태 조건을 파라미터화하면서 검색 경로가 바뀌지 않았는지
145+
`AccessibleDocumentQueryServiceTest`, `SearchFacadeTest`,
146+
`DocumentIndexingCompletionIntegrationTest`로 확인했다.
147+
148+
```bash
149+
./gradlew build
150+
```
151+
152+
760개 테스트 전부 통과.

0 commit comments

Comments
 (0)